Retrieving SecurityContext object by Dependency Injection - spring-boot

I am using Spring Security to protect my REST Apis.
I am able to retrieve the injected Principal object in my RestController:
#GetMapping("/user")
public ResponseEntity<User> getUser(Principal user) {
user.getName();
But unfortunately, the Principal object has no getAuthorities() method which I need to read out the user's role.
That's why I am using the static method to get the desired information what I need :
#GetMapping("/user")
public ResponseEntity<User> getUser() {
//how to get the SecurityContext by DI ?
Authentication user = SecurityContextHolder.getContext().getAuthentication();
Is there any easy solution to get the current SecurityContext object by Dependency injection ?

Please try following way:
#RequestMapping("/authentication")
Authentication getAuthentication(Authentication authentication) {
return authentication;
}
It's supported by ServletRequestMethodArgumentResolver, Authentication is child class of Principal
if (Principal.class.isAssignableFrom(paramType)) {
return request.getUserPrincipal();
}

Related

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

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.

Spring Boot - JWT authentication without db calls

Is it possible to implement simple JWT authentication (not caring about invalidating tokens - I'll do it in cache) without database calls to load user into Security Context? I see my current implementation hits database with every call to api (to load user into security context). Below you can see part of implementation of JwtAuthenticationFilter extending OncePerRequestFilter:
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
And here is the call to database, which I would like to avoid (with every authenticated call to api):
#Service
public class CustomUserDetailsService implements UserDetailsService {
#Autowired
UserRepository userRepository;
// This method is used by JWTAuthenticationFilter
#Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).orElseThrow(
() -> new UsernameNotFoundException("User not found with id : " + id)
);
return UserPrincipal.create(user);
}
}
I found some kind of solution of problem to build UserPrincipal object (it implements UserDetails interface) with only user id, username and granted authorities, and without e.g. password, which I cannot read from JWT token itself (the rest I can), but I am not sure if it's secure and and considered as a good-practice solution (UserDetails class requires password field and storing it JWT would not be wise I think). I need UserPrincipal instance (implementing UserDetails interface) to support as argument to UsernamePasswordAuthenticationToken, as you can see in the first paragraph.
One approach can be having two filter as follows.
CustomAuthenticationFilter that serves only login request/endpoint. You can do the following in this filter,
Call the db and validate the credential and retrieve the roles of the user
Generate the JWT token and you can store the user id or email along with roles in the subject of the JWT token. As we are adding user specific details I would recommend to encrypt the JWT token.
CustomAuthroizationFilter that serves all other requests/endpoints. You can do the following in this filter,
Validate JWT token
Retrieve the user id or email along with roles of the user from the subject of the JWT token.
Build spring authentication (UsernamePasswordAuthenticationToken) and set it in SecurityContextHolder like you did.
This way you will be calling db only during the login request not for all other api endpoints.

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

Spring security: what function does AuthenticationManager.authenticate() perform

I have been studying Spring security with JWT for a while and i noticed that at every tutorial I read, the username and password is taken, wrapped in a UsernamePasswordAuthenticationToken and passed on to a AuthenticationManager.authenticate() somthinglike this :
#RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(#RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword()));
// Reload password post-security so we can generate the token
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
// Return the token
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
my question is what does the authenticate method do, why is it used ?
From the Spring Security Reference:
AuthenticationManager is just an interface, so the implementation can be anything we choose. (...) The default implementation in Spring Security is called ProviderManager and rather than handling the authentication request itself, it delegates to a list of configured AuthenticationProviders, each of which is queried in turn to see if it can perform the authentication. Each provider will either throw an exception or return a fully populated Authentication object.

How to bypass UsernamePasswordAuthentication in Spring Security

I'm implementing an API that accepts a JWT as request parameter and on authentication, returns a new JWT.
#RequestMapping(value = "/authenticate/token", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
#Timed
public ResponseEntity authenticate(#RequestParam("login_token") final String token, HttpServletResponse response) {
LOG.debug("Request to login with token : {}", token);
try {
String jwt = authService.loginByToken(token);
response.addHeader(JWTConfigurer.AUTHORIZATION_HEADER, "Bearer " + jwt);
return ResponseEntity.ok(new IdentityToken(jwt));
} catch (AuthenticationException ae) {
LOG.trace("Authentication exception trace: {}", ae);
return new ResponseEntity<>(Collections.singletonMap("AuthenticationException",
ae.getLocalizedMessage()), HttpStatus.UNAUTHORIZED);
}
}
My loginByToken implementation looks as below
#Override public String loginByToken(String token) {
if (!tokenProvider.validateToken(token)) {
throw new BadCredentialsException("Token is invalid.");
}
SecureToken secureToken = tokenProvider.parseJwtToken(token);
User user = userRepository.findByEmail(secureToken.getEmail());
// TODO: Check Account Status is valid, User status is valid
Calendar c = Calendar.getInstance();
c.setTime(new Date());
c.add(Calendar.DATE, Constants.PASSWORD_EXPIRY_DAYS);
if (user.getPasswordExpiryDt() != null
&& user.getPasswordExpiryDt().after(c.getTime())) {
throw new BadCredentialsException("Password changed");
}
// TODO: Find how to create authentication object and return ID token.
// return tokenProvider.createToken(authentication, false);
return token;
}
At this point, I'm not sure how to create an authentication object that contains all user details that I could pass to createToken function that creates an identity token.
Here is my project without the changes mentioned in this post - https://github.com/santoshkt/ngx-pipes-test.
I have read about Anonymous Authentication, PreAuthenticated etc but not sure how to deal with this case. Will appreciate any pointers on how to do this.
If you want to use Spring Security, you should probably not use a Spring MVC endpoint to handle (pre-)authentication.
In your case you probably want to change your Spring security configuration so that it will have a filter that obtains your token from your request parameters and an authentication provider that retrieves the user/authentication object from your token:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/authenticate/token")
.authorizeRequests()
.anyRequest().authenticated()
.and()
// This is a filter bean you'll have to write
.addFilterBefore(filter(), RequestHeaderAuthenticationFilter.class)
// This is your token verifier/decoder
.authenticationProvider(authenticationProvider())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
For the filter you could extend from AbstractPreAuthenticatedProcessingFilter and make it return the login_token parameter. In here you have to implement two methods being getPreAuthenticatedPrincipal() and getPreAuthenticatedCredentials().
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// You could already decode your token here to return your username
return request.getParameter("login_token");
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return request.getParameter("login_token");
}
Your authentication provider should be of type PreAuthenticatedAuthenticationProvider and in here you can set an AuthenticationUserDetailsService:
#Bean
public AuthenticationProvider authenticationProvider() {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
// service is a bean of type AuthenticationUserDetailsService
// You could autowire this in your security configuration class
provider.setPreAuthenticatedUserDetailsService(service);
return provider;
}
Now you can create your own AuthenticationUserDetailsService to retrieve a UserDetails object based on your token:
#Service
public class TokenAuthenticationUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
#Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken authentication) throws UsernameNotFoundException {
// In this case the authentication.getCredentials() will contain your token and you can return a UserDetails object
return new User(/** ... */);
}
}
Since you want to provide the HTML page for the JWT token request the best approach is that you create you own Spring Security Custom Entry Point
You may give a look here for an example
If it's another system to manage the authentication and you want just manage the authorization you can "trust" the other System and then manage your own authorizations; in this case you can use the PreAuthentication Scenario as described here; you can find a sample here
I hope it's useful

Resources