How can I validate OAuth 2.0 token user details in #PreAuthorize annotation in Spring Boot REST service - spring

I need to make a check in #PreAuthorize annotation. Something like:
#PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
That is OK but I also need to validate some user details stored in the OAuth 2.0 token with those in the request path so I would need to do something like (oauthToken.userDetails is just an example:
#PreAuthorize("#pathProfileId.equals(oauthToken.userDetails.profileId)")
(profileId is not userId or userName, it is a user details that we add in the OAuth token when we create it)
What is the simplest way to make OAuth token properties visible in the preauthorized annotation security expression language?

You have two options:
1-
Setting UserDetailsService instance into DefaultUserAuthenticationConverter
and set converter to JwtAccessTokenConverter so when spring calls extractAuthentication method from DefaultUserAuthenticationConverter it found (userDetailsService != null) so it get the whole UserDetails object by calling implementation of loadUserByUsername when calling this line:
userDetailsService.loadUserByUsername((String) map.get(USERNAME))
implemented in next method inside spring class org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter.java but just adding it to clarify how spring get principal object from map (first getting it by username, and if userDetailsService not null so it get the whole object):
//Note: This method implemented by spring but just putting it to show where spring exctract principal object and how extracting it
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
if (userDetailsService != null) {
UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
authorities = user.getAuthorities();
principal = user;
}
return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
}
return null;
}
So what you need to implement in your microservice is:
#Bean//this method just used with token store bean example: new JwtTokenStore(tokenEnhancer());
public JwtAccessTokenConverter tokenEnhancer() {
/**
* CustomTokenConverter is a class extends JwtAccessTokenConverter
* which override "enhance" to add extra information to OAuth2AccessToken after
* authenticate the user and get it by loadUserByUsername implementation
* like profileId in your case
**/
JwtAccessTokenConverter converter = new CustomTokenConverter();
DefaultAccessTokenConverter datc = new DefaultAccessTokenConverter();
datc.setUserTokenConverter(userAuthenticationConverter());
converter.setAccessTokenConverter(datc);
//Other method code implementation....
}
#Autowired
private UserDetailsService userDetailsService;
#Bean
public UserAuthenticationConverter userAuthenticationConverter() {
DefaultUserAuthenticationConverter duac = new DefaultUserAuthenticationConverter();
duac.setUserDetailsService(userDetailsService);
return duac;
}
Note: this first way will hit database in every request so it load user by username and get UserDetails object so it assign it to principal object inside authentication.
2-
If for any reason you see it's better to not hit database in each request and no problem about executing data needed like profileId from token passed in request.
Assuming you know that old authorities assigned to user when generating oauth2 token will always be in token till it goes invalid even after you change it in database for user who passes the token in request so user could call a method not allowed to him/her anymore after extracting token and it was allowed before extracting the token.
So this means if user authorities changed after generating the token, new authorities will not be checked by #PreAuthorize as it's not removed or added to token and you have to wait till old token goes invalid or expired so user forced to execute the service again to get new oauth token.
Anyway, in this second option you only need to override extractAuthentication method inside CustomTokenConverter class extends JwtAccessTokenConverter and forget about setting access token converter converter.setAccessTokenConverter from tokenEnhancer() method in first option, and here are the whole CustomTokenConverter you can use it for reading data from token and return principal object not just string username:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
public class CustomTokenConverter extends JwtAccessTokenConverter {
// This is the method you need to override to read data direct from token passed in request
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Object userIdObj = map.get(AuthenticationUtils.USER_ID);
UUID userId = userIdObj != null ? UUID.fromString(userIdObj.toString()) : null;
Object profileIdObj = map.get(AuthenticationUtils.PROFILE_ID);
UUID profileId = profileIdObj != null ? UUID.fromString(profileIdObj.toString()) : null;
Object firstNameObj = map.get(AuthenticationUtils.FIRST_NAME);
String firstName = firstNameObj != null ? String.valueOf(firstNameObj) : null;
Object lastNameObj = map.get(AuthenticationUtils.LAST_NAME);
String lastName = lastNameObj != null ? String.valueOf(lastNameObj) : null;
JwtUser principal = new JwtUser(userId, profileId, authentication.getUserAuthentication().getName(), "N/A", authentication.getUserAuthentication().getAuthorities(), firstName, lastName);
authentication = new OAuth2Authentication(authentication.getOAuth2Request(),
new UsernamePasswordAuthenticationToken(principal, "N/A", authentication.getUserAuthentication().getAuthorities()));
return authentication;
}
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
JwtUser user = (JwtUser) authentication.getPrincipal();
Map<String, Object> info = new LinkedHashMap<>(accessToken.getAdditionalInformation());
if (user.getId() != null)
info.put(AuthenticationUtils.USER_ID, user.getId());
if (user.getProfileId() != null)
info.put(AuthenticationUtils.PROFILE_ID, user.getProfileId());
if (isNotNullNotEmpty(user.getFirstName()))
info.put(AuthenticationUtils.FIRST_NAME, user.getFirstName());
if (isNotNullNotEmpty(user.getLastName()))
info.put(AuthenticationUtils.LAST_NAME, user.getLastName());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
customAccessToken.setAdditionalInformation(info);
return super.enhance(customAccessToken, authentication);
}
private boolean isNotNullNotEmpty(String str) {
return Optional.ofNullable(str).map(String::trim).map(string -> !str.isEmpty()).orElse(false);
}
}
Finally: Guess how i know you are asking about JWT used with OAuth2?
Because i am a part of your company :P and you know that :P

Related

Spring Boot with Spring Security - Authorization with Method Level Security with #PreAuthorize, #RolledAllowed or #Secured Not Working

I have a Spring Boot application that users Spring Security. My Authentication and Authorization filters are working as expected.
In my Authentication filter, I generate JWT token with list of user authorities set as claim, and send the generated JWT together with claims back to client as part of "Auth" header. That is all working great.
In Authorization filter, I also got it all working fine, my doFilterInternals() override does proper chaining and it also calls my getAuthenticationToken() method:
private UsernamePasswordAuthenticationToken getAuthenticationToken(HttpServletRequest request) {
String token = request.getHeader("Auth");
if (token != null) {
token = token.replace("Bearer", "");
String username = Jwts.parser()
.setSigningKey(SecurityConstants.getTokenSecret())
.parseClaimsJws(token)
.getBody()
.getSubject();
String authoritiesString = Jwts.parser()
.setSigningKey(SecurityConstants.getTokenSecret())
.parseClaimsJws(token)
.getBody()
.get("user-authorities").toString(); //authority1, authority2, ...
if (username != null) {
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authoritiesString);
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
return null;
}
Above, I extract authorities (these are user groups coming from Active Directory) from claim I named"user-authorities" and I generate new UsernamePasswordAuthenticationToken with the authorities and return it.
This is all working great and I have been using for a while now.
Now, I am trying to use these authorities to add method level security to my controllers.
In order to do so, I have a #Configuration class which I annotated with #EnableGlobalMethodSecurity:
#Configuration
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class AppConfig {
...
}
Then on my controller I am using #Secured("authority1") to secure my controller:
#PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
#Secured("authority1")
public CarResponse saveCar(#Valid #RequestBody CarRequest carRequest, #RequestHeader HttpHeaders httpHeaders) {
System.out.println("Received :" + carRequest.toString());
return null;
}
I know JWT token contains claims with "authority1,authority2,authority3" comma-delimited string of authorities. So, my expectation would be that the controller below will execute for a user who authenticates and has these 3 authorities.
However, what I get back is 500 error. If I comment out the #Secured annotation, my controller will execute just fine but then it is not secured. I have also tried using #PreAuthorized("hasRole('authority1')") and also #RolesAllowed("authority1") but none are workng.
I dont know what I am missing.

Spring boot websocket: how to get the current principal programmatically?

By this thread I know that I can access to the principal by passing it as an argument to the method.
Nevetheless I need to access to this information in a transparent way, I tried with:
SecurityContextHolder.getContext().getAuthentication()
But it gives me null. So, isn't there another way?
It seems that, in order to obtain the full reference I have to define a custom channel interceptor:
private static class MyReceiver implements ChannelInterceptor{
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
SimpMessageType type = getType(message);
if(type == SimpMessageType.SUBSCRIBE) {
message.getHeaders().get("simpUser")); //it works here
}
return ChannelInterceptor.super.preSend(message, channel);
}
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
This will give you the current logged-in Username in Spring Security
Note :
UserDetails object is the one that Spring Security uses to keep user-related information.
SecurityContext is used to store the details of the currently authenticated user and SecurityContextHolder is a helper class that provides access to the security context

How to retrieve attributes and username sent by the CAS server with Spring Security

I have a spring boot application, which is MVC in nature. All page of this application are being authenticated by CAS SSO.
I have used "spring-security-cas" as described at https://www.baeldung.com/spring-security-cas-sso
Everything working fine as expected. However, I have one problem - that is, I cannot retrieve attributes
and username sent by the CAS server in the following #Bean. What need I do to retrieve all the attributes
and and username sent by the CAS server?
#Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(
s -> new User("casuser", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
return provider;
}
First you will need to configure the attributeRepository source and the attributes to be retrieved, in attributeRepository section in CAS server, like:
cas.authn.attributeRepository.jdbc[0].singleRow=false
cas.authn.attributeRepository.jdbc[0].sql=SELECT * FROM USERATTRS WHERE {0}
cas.authn.attributeRepository.jdbc[0].username=username
cas.authn.attributeRepository.jdbc[0].role=role
cas.authn.attributeRepository.jdbc[0].email=email
cas.authn.attributeRepository.jdbc[0].url=jdbc:hsqldb:hsql://localhost:9001/xdb
cas.authn.attributeRepository.jdbc[0].columnMappings.attrname=attrvalue
cas.authn.attributeRepository.defaultAttributesToRelease=username,email,role
Check this example from CAS blog.
Then you need to implement an AuthenticationUserDetailsService at the service to read attributes returned from CAS authentication, something like:
#Component
public class CasUserDetailService implements AuthenticationUserDetailsService {
#Override
public UserDetails loadUserDetails(Authentication authentication) throws UsernameNotFoundException {
CasAssertionAuthenticationToken casAssertionAuthenticationToken = (CasAssertionAuthenticationToken) authentication;
AttributePrincipal principal = casAssertionAuthenticationToken.getAssertion().getPrincipal();
Map attributes = principal.getAttributes();
String uname = (String) attributes.get("username");
String email = (String) attributes.get("email");
String role = (String) attributes.get("role");
String username = authentication.getName();
Collection<SimpleGrantedAuthority> collection = new ArrayList<SimpleGrantedAuthority>();
collection.add(new SimpleGrantedAuthority(role));
return new User(username, "", collection);
}
}
Then, adjust your authenticationProvider with provider.setAuthenticationUserDetailsService(casUserDetailService);

Spring OAuth/JWT get extra information from access token

I made a simple application that use spring security with oauth/jwt provider.
I added extra information in jwt token by custom JwtAccessTokenConverter and it works well.
My issue is how gets these extra informations in my Rest Controller.
This is my test:
#RequestMapping(value = "/test", produces = { "application/json" },method = RequestMethod.GET)
public String testMethod(OAuth2Authentication authentication,
OAuth2AccessToken token,
Principal user){
.....
Object a=token.getAdditionalInformation();
Object b=token.getValue();
...
}
The results are:
OAuth2Authentication: well inject but don't contain additional informations or accesstoken object (it contains only the original jwt token string).
User is a reference to OAuth2Authentication
OAuth2AccessToken: is aop proxy without any information infact object A and B are null.
Some extra info:
I checked,by debug, that ResourceService use my JwtAccessTokenConverter and extract the list of additional information from the access token string in input.
I found a possible solution.
I set in my JwtAccessTokenConverter a DefaultAccessTokenConverter where i set my custom UserTokenConverter.
So..
The JwtAccessTokenConverter manage only the jwt aspect of access token (token verification and extraction), the new DefaultAccessTokenConverter manages the oauth aspect of access token convertion including the use of my custom UserTokenConverter to create the Pricipal with custom informations extracted from jwt token.
public class myUserConverter extends DefaultUserAuthenticationConverter {
public Authentication extractAuthentication(Map<String, ?> map) {
if (map.containsKey(USERNAME)) {
// Object principal = map.get(USERNAME);
Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
UserDto utente = new UserDto();
utente.setUsername(map.get(USERNAME).toString());
utente.setUfficio(map.get("ufficio").toString());
utente.setExtraInfo(map.get("Informazione1").toString());
utente.setNome(map.get("nome").toString());
utente.setCognome(map.get("cognome").toString());
utente.setRuolo(map.get("ruolo").toString());
return new UsernamePasswordAuthenticationToken(utente, "N/A", authorities);
}
return null;
}

Log user in with remember-me functionality in Spring 3.1

I currently log users in programmatically (like when they login through Facebook or other means than using my login form) with:
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(user, "", authorities)
);
What I want to do instead is log the user in as if they set the remember-me option on in the login form. So I'm guessing I need to use the RememberMeAuthenticationToken instead of the UsernamePasswordAuthenticationToken? But what do I put for the key argument of the constructor?
RememberMeAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities)
UPDATE: I'm using the Persistent Token Approach described here. So there is no key like in the Simple Hash-Based Token Approach.
I assume you already have <remember-me> set in your configuration.
The way remember-me works is it sets a cookie that is recognized when the user comes back to the site after their session has expired.
You'll have to subclass the RememberMeServices (TokenBased or PersistentTokenBased) you are using and make the onLoginSuccess() public. For example:
public class MyTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
#Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
super.onLoginSuccess(request, response, successfulAuthentication);
}
}
<remember-me services-ref="rememberMeServices"/>
<bean id="rememberMeServices" class="foo.MyTokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<!-- etc -->
</bean>
Inject your RememberMeServices into the bean where you are doing the programmatic login. Then call onLoginSuccess() on it, using the UsernamePasswordAuthenticationToken that you created. That will set the cookie.
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, "", authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
getRememberMeServices().onLoginSuccess(request, response, auth);
UPDATE
#at improved upon this, with no subclassing of RememberMeServices:
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, "", authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
// This wrapper is important, it causes the RememberMeService to see
// "true" for the "_spring_security_remember_me" parameter.
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) {
#Override public String getParameter(String name) { return "true"; }
};
getRememberMeServices().loginSuccess(wrapper, response, auth);
This is the source for the constructor.
public RememberMeAuthenticationToken(String key, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
if ((key == null) || ("".equals(key)) || (principal == null) || "".equals(principal)) {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
this.keyHash = key.hashCode();
this.principal = principal;
setAuthenticated(true);
}
The key is hashed and its used to determine whether the authentication used for this user in the security context is not a 'forged' one.
Have a look at the RememberMeAuthenicationProvider source.
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
"The presented RememberMeAuthenticationToken does not contain the expected key"));
}
return authentication;
}
So to answer your question, you need to pass the hash code of the key field of the Authentication representing the user.

Resources