How can I require consent for each unique anonymous user with Spring Security OAuth2? - spring

My app has a singular endpoint. It triggers an OAuth2 authorization grant flow. It is meant to be called only by anonymous users. Each anonymous user represents a different person with different authorizations in the resource server. Consent (i.e., distinct authorization grant) is required from each anonymous user.
What is configuration in Spring Boot OAuth2 to require a consent for each anonymous user?
I'm using Spring Boot oath2-client 2.6.4 and Spring Security 5.6.2.
Currently, I have oauth2client configuration. It does not satisfy requirement. In this configuration, consent is requested only once and applied to all following anonymous callers. All callers share the same grant and access token.
I sense oauth2login may be the appropriate configuration, but I have needful customizations which I have to overcome before I try oauth2login. I have to disable the generated login page which prompts the user to select a provider, and I have to add custom fields to the authorization request. I have not had any success with these customizations in outh2login. So, this approach feels right, but is seemingly unavailable.
For information about this endpoint's caller, see: HL7 FHIR SMART-APP-LAUNCH

There are a number of challenges to this, related to:
My app has a singular endpoint. [...] It is meant to be called only by anonymous users.
This requirement makes it difficult for Spring Security to be of much help. This is because anonymous users typically don't have sessions, and the authorization_code grant is a flow which requires state and therefore a session. As a side note, I am not sure I fully understand how or why the specification you linked to (which is built on OAuth 2.0, as far as I can see) makes sense in the context of a client that allows an anonymous user.
Having said that, this seems possible using only the .oauth2Client() support in Spring Security if you create a custom filter for managing anonymous users. Note: The following assumes that the authorization server does not ignore the launch parameter even if a session exists in the browser.
The following configuration defines and configures this filter, as well as customizing the oauth2Client() to pass the launch parameter to the authorization server. It essentially creates a temporary authentication for the launch parameter to be saved as the principalName in the session for the duration of the flow.
#EnableWebSecurity
public class SecurityConfig {
private static final String PARAMETER_NAME = "launch";
private static final String ROLE_NAME = "LAUNCH_USER";
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().hasRole(ROLE_NAME)
)
.addFilterAfter(authenticationFilter(), AnonymousAuthenticationFilter.class)
.oauth2Client((oauth2) -> oauth2
.authorizationCodeGrant((authorizationCode) -> authorizationCode
.authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository))
)
);
return http.build();
}
private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
// Configure a request customizer for the OAuth2AuthorizationRequestRedirectFilter
authorizationRequestResolver.setAuthorizationRequestCustomizer((authorizationRequest) -> {
Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
// Customize request with principal name originally obtained from request parameter
if (currentAuthentication instanceof RequestParameterAuthenticationToken) {
Map<String, Object> additionalParameters = Map.of(PARAMETER_NAME, currentAuthentication.getName());
authorizationRequest.additionalParameters(additionalParameters);
}
});
return authorizationRequestResolver;
}
private RequestParameterAuthenticationFilter authenticationFilter() {
return new RequestParameterAuthenticationFilter(PARAMETER_NAME, AuthorityUtils.createAuthorityList("ROLE_" + ROLE_NAME));
}
/**
* Authentication filter that authenticates an anonymous request using a request parameter.
*/
public static final class RequestParameterAuthenticationFilter extends OncePerRequestFilter {
private final String parameterName;
private final List<GrantedAuthority> authorities;
public RequestParameterAuthenticationFilter(String parameterName, List<GrantedAuthority> authorities) {
this.parameterName = parameterName;
this.authorities = authorities;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
SecurityContext existingSecurityContext = SecurityContextHolder.getContext();
if (existingSecurityContext != null && !(existingSecurityContext.getAuthentication() instanceof AnonymousAuthenticationToken)) {
filterChain.doFilter(request, response);
return;
}
String principalName = request.getParameter(parameterName);
if (principalName != null) {
Authentication authenticationResult = new RequestParameterAuthenticationToken(principalName, authorities);
authenticationResult.setAuthenticated(true);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authenticationResult);
SecurityContextHolder.setContext(securityContext);
}
filterChain.doFilter(request, response);
}
}
/**
* Custom authentication token that can be persisted between requests, but is otherwise very similar to
* {#link AnonymousAuthenticationToken}.
*/
public static final class RequestParameterAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
private static final long serialVersionUID = 1L;
private final String principalName;
public RequestParameterAuthenticationToken(String principalName, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principalName = principalName;
}
#Override
public Object getPrincipal() {
return this.principalName;
}
#Override
public Object getCredentials() {
return this.principalName;
}
}
}
You can use this in a controller endpoint, as in the following example:
#RestController
public class LaunchController {
#GetMapping("/app/launch")
public void launch(
#RegisteredOAuth2AuthorizedClient("fhir-client")
OAuth2AuthorizedClient authorizedClient) {
String launchParameter = authorizedClient.getPrincipalName();
String accessToken = authorizedClient.getAccessToken().getTokenValue();
// Use authorizedClient.getAccessToken() to make a request (WebClient)...
// Clear the SecurityContext after the request, to force the next request
// to start the flow over again
SecurityContextHolder.clearContext();
}
}
See related issue #11069 for additional context on this answer.

Related

Spring boot authorization returns 403 for any authorization request using #RolesAllowed, #Secured or #PreAuthorize

I've been working from this article (and a few other similar ones): https://medium.com/omarelgabrys-blog/microservices-with-spring-boot-authentication-with-jwt-part-3-fafc9d7187e8
The client is an Angular 8 app which acquires a Jwt from an independent microservice. Trying to add filter(s) to a different microservice to require specific authorization via jwt roles.
Consistently receiving 403 errors.
Security Config:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true,
securedEnabled = true,
jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurityConfig() {}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and().csrf().disable()
// make sure we use stateless session; session won't be used to store user's state.
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// Add a filter to validate the tokens with every request
.addFilterAfter(new JwtAuthorizationFilter2(), UsernamePasswordAuthenticationFilter.class)
// authorization requests config
.authorizeRequests()
// Any other request must be authenticated
.anyRequest().authenticated();
}
}
Filter:
public class JwtAuthorizationFilter2 extends OncePerRequestFilter {
private final String HEADER = "Authorization";
private final String PREFIX = "Bearer ";
private final String SECRET = "foo";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader(SecurityConstants.HEADER_STRING);
if (token != null) {
// parse the token.
DecodedJWT decoded = JWT.require(Algorithm.HMAC512(SecurityConstants.SECRET.getBytes()))
.build()
.verify(token.replace(SecurityConstants.TOKEN_PREFIX, ""));
String user = decoded.getSubject();
List<SimpleGrantedAuthority> sgas = Arrays.stream(
decoded.getClaim("roles").asArray(String.class))
.map( s -> new SimpleGrantedAuthority(s))
.collect( Collectors.toList());
if (sgas != null) {
sgas.add(new SimpleGrantedAuthority("FOO_Admin"));
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
user,
null,
sgas);
SecurityContextHolder.getContext().setAuthentication(auth);
}
else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
}
}
}
This code works fine without any authorization requirements defined, but if an authorization is defined in WebSecurityConfig, or by decorating a controller method, http 403 is received for all requests in scope.
Examples:
.authorizeRequests().antMatchers("/**").hasRole("FOO_Admin")
// or any of these
#PreAuthorize("hasRole('FOO_Admin')")
#RolesAllowed({"FOO_Admin"})
#Secured({"FOO_Admin"})
Device get(#PathVariable String id) {
// some code
}
When code is halted at SecurityContextHolder.getContext().setAuthentication(auth),
auth.authenticated = true
and
auth.authorities includes a SimpleGrantedAuthority for "FOO_Admin"
So I'm wondering whether:
The FilterChain needs an Authentication Filter (or does authentication occur in JwtAuthorizationFilter2?)?
There is not a spelling or formatting or capitalization difference to role name.
I'm stupefied. Any help would be greatly appreciated.
#PreAuthorize("hasRole('FOO_Admin')) expects the user has an authority ROLE_FOO_Admin, which will be prefixed by ROLE_. However, the user only has the authority FOO_Admin , hence it fails to access the method.
You have several options:
(1) Change the prefix by declaring a GrantedAuthorityDefaults bean:
#Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("FOO");
}
And use #PreAuthorize(hasRole('Admin')) to secure the method.
(2) Or more simpler is to use #PreAuthorize("hasAuthority('FOO_Admin')") , which will directly check if the user has the authority FOO_Admin , without adding any prefix to it.
P.S JwtAuthorizationFilter2 only verifies if an user is valid and get the related user information which prepare for the authorization user later. It is an authentication and I would rename it to JwtAuthenticationFilter2 to describe more exactly what it does actually.

REST API with both apiKey and username/password authentication

I'm a Spring's beginner and I'm trying to build a REST API, connected to a React frontend in order to learn these technologies.
In order to secure this API, I added an apiKey mechanism with Spring Security, by creating a filter that checks a specific header key (API-KEY in this case), and that only allows requests that match the correct api key value.
I added this filter in my security config, which extends WebSecurityConfigurerAdapter. However, I'd like to add another authentication mechanism just to authenticate my users, in a traditional username/password way. I'm a bit lost, I read a lot of articles but all of these are using the same mechanism (filter + configure the security component). But I really don't know how to gather these two mechanisms.
I would like all requests are intercepted to check the API-KEY value, but I also would like to have an anonymous and authenticated parts in my app.
How could I achieve this ? I found some elements like interceptors but it seems to be only available for spring-mvc app.
Here's the filter I'm using :
public class ApiKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* The request header we want to check with our apiKey
*/
private String principalRequestHeader;
public ApiKeyAuthFilter(String principalRequestHeader) {
this.principalRequestHeader = principalRequestHeader;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader(principalRequestHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "N/A";
}
}
And here's my security config :
#Configuration
#EnableWebSecurity
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* The header corresponding to our apiKey
*/
#Value("${application.security.requestKey}")
private String apiKeyHeader;
/**
* The api key value we want to test with the header value
*/
#Value("${application.security.apiKey}")
private String apiKeyValue;
Logger logger = LoggerFactory.getLogger(ApiSecurityConfig.class);
#Override
protected void configure(HttpSecurity http) throws Exception {
ApiKeyAuthFilter filter = new ApiKeyAuthFilter(this.apiKeyHeader);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
final String principal = (String) authentication.getPrincipal();
if (!apiKeyValue.equals(principal)) {
throw new BadCredentialsException("The API key was not found or doesn't match the correct value");
}
logger.info("Connexion autorisée");
authentication.setAuthenticated(true);
return authentication;
}
});
http.cors().and().
antMatcher("/api/**").
csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).
and().
addFilter(filter).
authorizeRequests().anyRequest().authenticated();
}
}
Do you have any clue to setup this kind of authentication ? I saw that we could define an order in our filter with methods like addFilterAfter() or addFilterBefore(), but I don't know how to setup this with my usecase.
I also found this post : How to config multiple level authentication for spring boot RESTful web service?
which seems to have the same requirements, I tried the solution provided but the authentication isn't dynamic (it's only using a string "valid-user" for its authentication filter, and I need to authenticate through my User entity stored in an in-memory h2 database. How to achieve this ?
Thank's a lot for your answers and have a nice day !

Spring secure endpoint with only client credentials (Basic)

I have oauth2 authorization server with one custom endpoint (log out specific user manually as admin)
I want this endpoint to be secured with rest client credentials (client id and secret as Basic encoded header value), similar to /oauth/check_token.
This endpoint can be called only from my resource server with specific scope.
I need to check if the client is authenticated.
I would like to be able to add #PreAuthorize("#oauth2.hasScope('TEST_SCOPE')")on the controller`s method.
I could not find any docs or way to use the Spring`s mechanism for client authentication check.
EDIT 1
I use java config not an xml one
So I ended up with the following solution
Authentication Manager
public class ClientAuthenticationManager implements AuthenticationManager {
private ClientDetailsService clientDetailsService;
private PasswordEncoder passwordEncoder;
public HGClientAuthenticationManager(ClientDetailsService clientDetailsService, PasswordEncoder passwordEncoder) {
Assert.notNull(clientDetailsService, "Given clientDetailsService must not be null!");
Assert.notNull(passwordEncoder, "Given passwordEncoder must not be null!");
this.clientDetailsService = clientDetailsService;
this.passwordEncoder = passwordEncoder;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
ClientDetails clientDetails = null;
try {
clientDetails = this.clientDetailsService.loadClientByClientId(authentication.getPrincipal().toString());
} catch (ClientRegistrationException e) {
throw new BadCredentialsException("Invalid client id or password");
}
if (!passwordEncoder.matches(authentication.getCredentials().toString(), clientDetails.getClientSecret())) {
throw new BadCredentialsException("Invalid client id or password");
}
return new OAuth2Authentication(
new OAuth2Request(null, clientDetails.getClientId(), clientDetails.getAuthorities(), true,
clientDetails.getScope(), clientDetails.getResourceIds(), null, null, null),
null);
}
}
Filter declaration
private BasicAuthenticationFilter basicAuthenticationFilter() {
ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(
this.clientDetailsService);
clientDetailsUserDetailsService.setPasswordEncoder(this.passwordEncoder);
return new BasicAuthenticationFilter(
new ClientAuthenticationManager(this.clientDetailsService, this.passwordEncoder));
}
Filter registration
httpSecurity.addFilterBefore(this.basicAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
WARNING!!!
This will prevent any other types of authentication (oauth2, etc.).
ONLY Basic authentication is accepted and ONLY for registered clients.
#PreAuthorize("#oauth2.hasScope('TEST_SCOPE')") On the controller method should be sufficiënt. If the client is not authenticated, no scope is available and the scope check will fail.
If you want, you can use the Spring Security expression #PreAuthorize("isAuthenticated()") to check if a client is authenticated: https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/#el-common-built-in
You could also configure the HttpSecurity instead of working with #PreAuthorize

Can we override CheckTokenEndpoint and provide custom CheckTokenEndpoint in Spring Oauth2?

My application has separate authorization server and resource server. Authorization server provides access token to resource server. Resource server then sends the request for protected resource with access token.
Resource server uses RemoteTokenServices to validate whether the access token is proper or not.
#Bean
public RemoteTokenServices remoteTokenServices(final #Value("${auth.server.url}") String checkTokenUrl,
final #Value("${auth.server.clientId}") String clientId,
final #Value("${auth.server.clientsecret}") String clientSecret)
{
final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(checkTokenUrl+"?name=value");
remoteTokenServices.setClientId(clientId);
remoteTokenServices.setClientSecret(clientSecret);
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
return remoteTokenServices;
}
application.yml
auth:
server:
url: http://localhost:9191/api/oauth/check_token/
clientId: clientid
clientsecret: secret
I want to pass additional parameter like resource id so that I can verify if the token is authorized for that resource or not.
I want to get that parameter in org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint,
and want override below method to add some logic. Is it possible?
#RequestMapping(value = "/oauth/check_token")
#ResponseBody
public Map<String, ?> checkToken(#RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
return response;
}
How to send some parameter to oauth/check_token and override checkToken() method?
Basically what I am doing is when access token is generated, I am saving some record about the resources that token is allowed for.
When I receive the request for the resource on resource server, I want to pass the resource id to auth server and want to verify the token is authorized for that resource or not?
I've just create new CustomCheckTokenEndpoint and copy whole code of CheckTokenEndpoint then override checkToken(...) method
#Configuration
#EnableAuthorizationServer
public class CustomAuthorizationServer extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.pathMapping("/oauth/check_token", "/my/oauth/check_token");
}
}
**CustomCheckTokenEndpoint.java**
public class CustomCheckTokenEndpoint {
// copy whole CheckTokenEndpoint
#RequestMapping(value = "/my/oauth/check_token")
#ResponseBody
public Map<String, ?> checkToken(String value) {
// your code will be here
}
}

How to get/set the principal and session attributes from Spring 4 stomp websocket methods

I'm doing experiments with Spring 4 websockets and stomp, and I have a hard time figuring out how to get/set the current user and other session attributes in a message handling method annotated with #MessageMapping.
The documentation says that the message handling methods can take a Principal as argument, and I found that the principal is retrieved by Spring by calling getUserPrincipal() on the native socket session, and then associated with the socket session, but I haven't found any way to easily customize this behavior, other than writing a servlet filter and wrap the original request into a wrapper returning the principal found in my cookie.
So my questions are:
How to manually set the principal to the socket session, when the client connects (I have this information thanks to a custom cookie, and I don't use Spring security)?
If 1 is not possible, how to add additional attributes to the socket session when the client connects?
How to access the socket session and its attributes from a message handling method?
Is there a way to access the login and passcode sent by the browser at connection time. They seem to be completely ignore by Spring and not accessible.
UPDATE: With Spring 4.1 it is possible to set the user on the handshake for #1 from above. Per the Spring documentation you can create a new class which extends DefaultHandshakeHandler and override the determineUser method. Additionally you can also create a security filter which sets the principal as well if you have a token. I have implemented the second one myself and I include some sample code for both below.
For #2 and #3 I do not think that it is possible still. For #4 Spring intentionally ignores these per the documentation here.
SAMPLE CODE FOR DefaultHandshakeHandler SUBCLASS:
#Configuration
#EnableWebSocketMessageBroker
public class ApplicationWebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
public class MyHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
// add your own code to determine the user
return null;
}
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/myEndPoint").setHandshakeHandler(new MyHandshakeHandler());
}
}
SAMPLE CODE FOR SECURITY FILTER:
public class ApplicationSecurityTokenFilter extends GenericFilterBean {
private final static String AUTHENTICATION_PARAMETER = "authentication";
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
// check to see if already authenticated before trying again
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
if ((existingAuth == null) || !existingAuth.isAuthenticated()) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
UsernamePasswordAuthenticationToken token = extractToken(request);
// dump token into security context (for authentication-provider to pick up)
if (token != null) { // if it exists
SecurityContextHolder.getContext().setAuthentication(token);
}
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
private UsernamePasswordAuthenticationToken extractToken( HttpServletRequest request ) {
UsernamePasswordAuthenticationToken authenticationToken = null;
// do what you need to extract the information for a token
// in this example we assume a query string that has an authenticate
// parameter with a "user:password" string. A new UsernamePasswordAuthenticationToken
// is created and then normal authentication happens using this info.
// This is just a sample and I am sure there are more secure ways to do this.
if (request.getQueryString() != null) {
String[] pairs = request.getQueryString().split("&");
for (String pair : pairs) {
String[] pairTokens = pair.split("=");
if (pairTokens.length == 2) {
if (AUTHENTICATION_PARAMETER.equals(pairTokens[0])) {
String[] tokens = pairTokens[1].split(":");
if (tokens.length == 2) {
log.debug("Using credentials: " + pairTokens[1]);
authenticationToken = new UsernamePasswordAuthenticationToken(tokens[0], tokens[1]);
}
}
}
}
}
return authenticationToken;
}
}
// set up your web security for the area in question
#Configuration
public class SubscriptionWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/myEndPoint**","/myEndPoint/**").and()
.addFilterBefore(new ApplicationSecurityTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic() // leave this if you want non web browser clients to connect and add an auth header
.and()
.csrf().disable();
}
}
** NOTE: ** DO NOT declare your filter as a Bean. If you do then it will also be picked up (at least using Spring Boot) in the generic filters so it will fire on every request.
This is impossible for the time being (Spring 4.0). An issue has been opened (and considered) at Spring: https://jira.springsource.org/browse/SPR-11228

Resources