I'm writing Spring Boot REST API, and I'm using JWT tokens. Now, I'm trying to create role-based authorization.
This is the tutorial/implementation that I'm using.
https://auth0.com/blog/implementing-jwt-authentication-on-spring-boot/
I expanded this implementation with additional Role entity, and added #ManyToMany mapping to ApplicationUser Entity.
Now, as far as I understands, user roles should be added to token (during its creation).
So, this is an existing code:
#Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
String token = JWT.create()
.withSubject(((User) auth.getPrincipal()).getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(HMAC512(SECRET.getBytes()));
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
I guess user roles should be added there. There is a function:
withArrayClaim(String Name, String[] items)
And there's my first problem: I'm not sure how to properly add this.
Then, is this fragments, which as far as I understand is place where token is verified:
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = JWT.require(Algorithm.HMAC512(SECRET.getBytes()))
.build()
.verify(token.replace(TOKEN_PREFIX, ""))
.getSubject();
if (user != null) {
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
}
return null;
}
return null;
}
What's bother me is fragment:
return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
I don't understand why there is null (Inteliij highlights it as 'credentials') and this new ArrayList. Is there a place, where I should fetch roles from token, and add them?
I know, it's kinda broad scope question, but i couldn't find other solutions.
Or mayby there is an easier way to create simple JWT token authenitcation/authorization (role based).
Looking forward for your answers!
EDIT:
Or mayby is there more simple solutioni - not keeping user roles inside key - but only adding them in this 'second' part where null and new ArrayList is?
Just create the granted authorities based in the user roles and authenticate the user with it. Then the authenticated user principal will contain the roles.
Simple example:
UserEntity userEntity = userRepository.findUserByEmail(user); // this depends of course on your implementation
if (userEntity == null) return null;
List<RoleEntity> roles = userEntity.getRoles();
Collection<GrantedAuthority> authorities = new HashSet<>();
roles.forEach((role) -> {
authorities.add(new SimpleGrantedAuthority(role.getName()));
});
return new UsernamePasswordAuthenticationToken(user, null, authorities);
Even better, you can create a UserPrincipal that implements UserDetails from spring security.
public class UserPrincipal implements UserDetails {
private static final long serialVersionUID = 1L;
private final UserEntity userEntity;
public UserPrincipal(UserEntity userEntity){
this.userEntity = userEntity;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new HashSet<>();
// Get user Roles
Collection<RoleEntity> roles = userEntity.getRoles();
if(roles == null) return authorities;
roles.forEach((role) -> {
authorities.add(new SimpleGrantedAuthority(role.getName()));
});
return authorities;
}
#Override
public String getPassword() {
return this.userEntity.getEncryptedPassword();
}
#Override
public String getUsername() {
return this.userEntity.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return false;
}
#Override
public boolean isAccountNonLocked() {
return false;
}
#Override
public boolean isCredentialsNonExpired() {
return false;
}
#Override
public boolean isEnabled() {
return false;
}
}
And to use it:
UserEntity userEntity = userRepository.findUserByEmail(user);
if (userEntity == null) return null;
UserPrincipal userPrincipal = new UserPrincipal(userEntity);
return new UsernamePasswordAuthenticationToken(userPrincipal, null, userPrincipal.getAuthorities());
Related
I am hoping someone can give me a more concrete example than the one I found in the documentation.
Using SpringBoot/Spring Security 5.6.0. I am migrating the authentication process based on SpringSecurity/SAML to SAML2.
I need to add to the Authentication a UserDetails built from the responseToken information.
Something like what we can read in the documentation: https://docs.spring.io/spring-security/reference/5.6.0-RC1/servlet/saml2/index.html#servlet-saml2login-opensamlauthenticationprovider-userdetailsservice
But I don't understand the third point: return a custom authentication that includes the user details: "return MySaml2Authentication(userDetails, authentication);"
In any case you would have to do "return new MySaml2Authentication(userDetails, authentication);" right?
In any case, when the process continues it is executed:
Authentication authenticate = provider.authenticate(authentication);
Which as we can see replaces the Details value with the original.
authenticationResponse.setDetails(authentication.getDetails());
/**
* #param authentication the authentication request object, must be of type
* {#link Saml2AuthenticationToken}
* #return {#link Saml2Authentication} if the assertion is valid
* #throws AuthenticationException if a validation exception occurs
*/
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
Saml2AuthenticationToken token = (Saml2AuthenticationToken) authentication;
String serializedResponse = token.getSaml2Response();
Response response = parse(serializedResponse);
process(token, response);
AbstractAuthenticationToken authenticationResponse = this.responseAuthenticationConverter
.convert(new ResponseToken(response, token));
if (authenticationResponse != null) {
authenticationResponse.setDetails(authentication.getDetails());
}
return authenticationResponse;
}
catch (Saml2AuthenticationException ex) {
throw ex;
}
catch (Exception ex) {
throw createAuthenticationException(Saml2ErrorCodes.INTERNAL_VALIDATION_ERROR, ex.getMessage(), ex);
}
}
How can you add a UserDetails that depends on the information obtained from the tokens?
I can't extend OpenSaml4AuthenticationProvider because it is "final".
The only thing I can think of is the option: https://docs.spring.io/spring-security/reference/5.6.0-RC1/servlet/saml2/index.html#servlet-saml2login-authenticationmanager-custom
And in the MySaml2AuthenticationManager set the Details after executing the Authentication authenticate = provider.authenticate(authentication); but it doesn't seem right to me.
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(responseToken -> {
Saml2Authentication auth = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter()// First, call the default converter, which extracts attributes and authorities from the response
.convert(responseToken);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
String role = getRole(auth.getName());
grantedAuthorities.add(new SimpleGrantedAuthority(role));
Saml2Authentication saml2Authentication = new Saml2Authentication((AuthenticatedPrincipal) auth.getPrincipal(), auth.getSaml2Response(), grantedAuthorities);
/*
//The details are replaced by the authentication.getDetails().
MyUserDetails userDetails = new MyUserDetails(authenticate.getName(),auth.getPrincipal());
saml2Authentication.setDetails(userDetails);
*/
return saml2Authentication;
});
Authentication authenticate = provider.authenticate(authentication);
//Doesn't sound like a good idea
if ((authenticate.getPrincipal() instanceof DefaultSaml2AuthenticatedPrincipal)) {
DefaultSaml2AuthenticatedPrincipal samlPrincipal = (DefaultSaml2AuthenticatedPrincipal) authenticate.getPrincipal();
MyUserDetails userDetails = new MyUserDetails(authenticate.getName(),
samlPrincipal.getFirstAttribute("SMFIRSTNAME")
, samlPrincipal.getFirstAttribute("SMLASTNAME")
, samlPrincipal.getFirstAttribute("SMEMAIL"));
((Saml2Authentication)authenticate).setDetails(defaultDISUserDetails);
}
return authenticate;
Any better option?
Thank you very much for your help
Best regards
I figured this out way too late as well. But you should actually use your own implementation of an AbstractAuthenticationToken and use an extra field to stuff your own object in. Here is mine:
public class CustomSaml2Authentication extends Saml2Authentication {
private User user;
public CustomSaml2Authentication(AuthenticatedPrincipal principal, String saml2Response,
Collection<? extends GrantedAuthority> authorities) {
super(principal, saml2Response, authorities);
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
I have a following method in the controller
#GetMapping("/hello")
#PreAuthorize("hasRole('ADMIN')")
public String hello() {
return "Hello " + JWTRequestFilter.UserClaim;
}
When a user who has the ADMIN role tries to access the /hello, 403 is returned. I have enabled the following in the websecurity class.
#EnableGlobalMethodSecurity(prePostEnabled = true)
Below is the JWT token.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZW50aHVyYW4iLCJSb2xlcyI6WyJBRE1JTiIsIlVTRVIiXSwiZXhwIjoxNTkzMDE0NDE5LCJpYXQiOjE1OTI5Nzg0MTl9.-7lTav3Nux8WVafUBGXjOxtXcE-r0fpfjb7wM7hrg6w
Even the JWT token has the role but still i'm getting 403. Does this preauthorize annotation see the role from the JWT or does it make a DB call and check the role of a user.Even I have used the #PreAuthrize annotation but still getting the same behaviour. How to resolve this 403. Below I have attached the JWTRequestFilter class.
public class JWTRequestFilter extends OncePerRequestFilter {
#Autowired
private MyUserDetailService userDetailService;
#Autowired
private JWTUtil jwtUtil;
public static String UserClaim = "";
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
UserClaim = username;
}
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails= this.userDetailService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
This is how I'm generating the JWT token and how I set the roles.
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
Set<String> Userroles = new HashSet<>();
User user = userRepository.findByUsername(userDetails.getUsername());
for(Role role:user.getRoles()){
Userroles.add(role.getName());
}
claims.put("Roles",Userroles.toArray());
return createToken(claims, userDetails.getUsername());
}
Suggested Approach to identify the issue
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails= this.userDetailService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
String authorities = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining());
System.out.println("Authorities granted : " + authorities);
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
System.out.println("Not Valid Token);
}
} else {
System.out.println("No Token);
}
Outcome: Token was valid but authorities were not loaded
Authorities granted :
Suggested Solution
Fix the MyUserDetailService to load Authorities in userDetails
Spring adds the prefix ROLE_ to the authorities.
You can either implement a setter that appends the role prefix.
Or another much simple way to do it would be to have a separate classe that implements GrantedAuthority interface
public class UserRole implements GrantedAuthority {
private MyRole role;
#Override
public String getAuthority() {
return "ROLE_" + role.toString();
}
}
//MyRole is the enum with the different roles ADMIN,VIEWER,...
I want to customize login API in spring boot. For a single kind of user, I created a implementation of UserDetailsService and it worked perfectly fine. Now, I want to create 3 different kinds of users, i.e., 3 different authorities. I don't think a single implementation can help me here. If I create 3 different implementations, and try using #Qualifier, how do I call a specific implementation ?
Any sort of help is appreciated! Below is the code for Login Endpoint of single kind Of user.
private static Logger logger = LogManager.getLogger();
#Value("${jwt.expires_in}")
private int EXPIRES_IN;
#Autowired
AuthenticationManager authManager;
#Autowired
TokenHelper tokenHelper;
#Autowired
ObjectMapper objectMapper;
#Autowired
PrincipalRepository principalRepository;
private boolean isAuthenticated(Authentication authentication) {
return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated();
}
#PostMapping("/principal")
public ResponseEntity<Object[]> loginPrincipal(#RequestParam(name ="username") String username,
#RequestParam(name ="password") String password){
logger.info("In login api");
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
logger.error("Invalid Request!");
return ResponseEntity.badRequest().header("reason", "bad request").body(null);
}
UsernamePasswordAuthenticationToken authReq =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authManager.authenticate(authReq);
boolean isAuthenticated = isAuthenticated(authentication);
if (!isAuthenticated) {
logger.error("Not authenticated");
return ResponseEntity.badRequest().body(null);
}
Principal principal = null;
try {
principal = principalRepository.findByUserName(username);
}catch(Exception e) {
logger.error("Couldn't retrieve user");
return ResponseEntity.badRequest().header("reason", "username not found").body(null);
}
String jwt = tokenHelper.generateToken( username );
SecurityContextHolder.getContext().setAuthentication(authentication);
UserTokenState userTokenState = new UserTokenState(jwt, EXPIRES_IN);
return ResponseEntity.accepted().body(new Object[] {userTokenState, principal.getPrincipalID()});
}
Below is the code for UserDetailsService Implementation:
#Service
public class UserDetailServiceImpl implements UserDetailsService {
#Autowired
private PrincipalRepository principalRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("Loading user from db");
Principal principal = principalRepository.findByUserName(username);
if( principal == null){
System.out.println("User not found");
throw new UsernameNotFoundException("No user found. Username tried: " + username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<GrantedAuthority>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_PRINCIPAL"));
System.out.println("All done");
return new org.springframework.security.core.userdetails.User(principal.getUserName(), principal.getPassword(), grantedAuthorities);
}
}
Here, I am fetching a principal from db, because this implementation is principal-specific. I wanna create similar implementations for Student and Teacher and use them accordingly.
You don't need to create more than one implementation for UserDetailsService. Student, Teacher are also users, only one thing will differ these users is "authorities"(role & authorities) in the application if we look at from general view. Spring Security firstly checks "username" and "password" for authentication and after successful authentication, it checks "authorities" for authorization process in order to allow to use resources(methods, and etc) according to the business logic of the application.
I had thought that my authorization implementation was done but when attempting to retrieve the UserDetails object, all I'm getting is the username.
I'm using oauth with the following particulars.
Configuring the AuthenticationManager:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
With this done, I can debug into my userDetailsService:
#Service
public class UserServiceImpl implements UserService, UserDetailsService {
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
MyUser persistedUser = userRepository.findByEmail(email);
if (persistedUser == null) {
throw new UsernameNotFoundException(String.format("The email %s doesn't exist", email));
}
List<GrantedAuthority> authorities = new ArrayList<>();
MyUser inMemoryUser = new MyUser(persistedUser.getEmail(), null, persistedUser.getEnabled(), false,
false, false, authorities);
return inMemoryUser;
}
}
This completes fine and my client gets back the JWT. But I found the following problem when debugging a later controller method.
#GetMapping
public #ResponseBody Iterable<Curriculum> getMyCurriculums(#AuthenticationPrincipal MyUser injectedUser) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
MyUser principle = (MyUser) auth.getPrincipal();
return curriculumService.findByUser(principle);
}
In this case, injectedUser = null, auth is an OAuth2Authentication, and principle is a String - the username. It should be MyUser
You should configure Spring Security to decode jwt token into MyUser object.
First define a custom OAuth2Authentication to encapsulate MyUser.
public class OAuth2AuthenticationUser extends OAuth2Authentication {
private MyUser myUser;
public OAuth2AuthenticationUser(OAuth2Request storedRequest, Authentication userAuthentication) {
super(storedRequest, userAuthentication);
}
public MyUser getMyUser() {
return myUser;
}
public void setMyUser(MyUser) {
this.myUser= myUser;
}
}
Then in a Security Configuration class configure jwt token decoding as follows:
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("SIGNING_KEY");
converter.setAccessTokenConverter(getAuthenticationAccessTokenConverter());
return converter;
}
private DefaultAccessTokenConverter getAuthenticationAccessTokenConverter() {
return new DefaultAccessTokenConverter() {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = (OAuth2Authentication) super.extractAuthentication(map);
OAuth2AuthenticationUser authenticationUser =
new OAuth2AuthenticationUser(authentication.getOAuth2Request(), authentication.getUserAuthentication());
MyUser myUser = new MyUser();
// Example properties
myUser.setId(map.get("id") != null ? Long.valueOf(map.get("id").toString()) : null);
myUser.setUsername(map.get("user_name") != null ? map.get("user_name").toString() : null);
myUser.setFullName(map.get("fullName") != null ? map.get("fullName").toString() : null);
myUser.setCustomerId(map.get("customerId") != null ? Long.valueOf(map.get("customerId").toString()) : null);
myUser.setCustomerName(map.get("customerName") != null ? map.get("customerName").toString() : null);
// More other properties
authenticationUser.setMyUser(myUser);
return authenticationUser;
}
};
}
And then you have access to MyUser object from Spring Security context as follows:
private static MyUser getMyUser() {
OAuth2AuthenticationUser authentication = (OAuth2AuthenticationUser) SecurityContextHolder.getContext().getAuthentication();
return (authentication != null && authentication.getMyUser() != null ? authentication.getMyUser() : new MyUser());
}
This fits well in a stateless environment as database access for user details is minimized and all you need is jwt token.
I have a resource server configured with #EnableResourceServer annotation and it refers to authorization server via user-info-uri parameter as follows:
security:
oauth2:
resource:
user-info-uri: http://localhost:9001/user
Authorization server /user endpoint returns an extension of org.springframework.security.core.userdetails.User which has e.g. an email:
{
"password":null,
"username":"myuser",
...
"email":"me#company.com"
}
Whenever some resource server endpoint is accessed Spring verifies the access token behind the scenes by calling the authorization server's /user endpoint and it actually gets back the enriched user info (which contains e.g. email info, I've verified that with Wireshark).
So the question is how do I get this custom user info without an explicit second call to the authorization server's /user endpoint. Does Spring store it somewhere locally on the resource server after authorization or what is the best way to implement this kind of user info storing if there's nothing available out of the box?
The solution is the implementation of a custom UserInfoTokenServices
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java
Just Provide your custom implementation as a Bean and it will be used instead of the default one.
Inside this UserInfoTokenServices you can build the principal like you want to.
This UserInfoTokenServices is used to extract the UserDetails out of the response of the /usersendpoint of your authorization server. As you can see in
private Object getPrincipal(Map<String, Object> map) {
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
return map.get(key);
}
}
return "unknown";
}
Only the properties specified in PRINCIPAL_KEYS are extracted by default. And thats exactly your problem. You have to extract more than just the username or whatever your property is named. So look for more keys.
private Object getPrincipal(Map<String, Object> map) {
MyUserDetails myUserDetails = new myUserDetails();
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
myUserDetails.setUserName(map.get(key));
}
}
if( map.containsKey("email") {
myUserDetails.setEmail(map.get("email"));
}
//and so on..
return myUserDetails;
}
Wiring:
#Autowired
private ResourceServerProperties sso;
#Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
return new MyUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
!!UPDATE with Spring Boot 1.4 things are getting easier!!
With Spring Boot 1.4.0 a PrincipalExtractor was introduced. This class should be implemented to extract a custom principal (see Spring Boot 1.4 Release Notes).
All the data is already in the Principal object, no second request is necessary. Return only what you need. I use the method below for Facebook login:
#RequestMapping("/sso/user")
#SuppressWarnings("unchecked")
public Map<String, String> user(Principal principal) {
if (principal != null) {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
Authentication authentication = oAuth2Authentication.getUserAuthentication();
Map<String, String> details = new LinkedHashMap<>();
details = (Map<String, String>) authentication.getDetails();
logger.info("details = " + details); // id, email, name, link etc.
Map<String, String> map = new LinkedHashMap<>();
map.put("email", details.get("email"));
return map;
}
return null;
}
In the Resource server you can create a CustomPrincipal Class Like this:
public class CustomPrincipal {
public CustomPrincipal(){};
private String email;
//Getters and Setters
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Implement a CustomUserInfoTokenServices like this:
public class CustomUserInfoTokenServices implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private final String userInfoEndpointUrl;
private final String clientId;
private OAuth2RestOperations restTemplate;
private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
private PrincipalExtractor principalExtractor = new CustomPrincipalExtractor();
public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
this.userInfoEndpointUrl = userInfoEndpointUrl;
this.clientId = clientId;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public void setRestTemplate(OAuth2RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {
Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null");
this.authoritiesExtractor = authoritiesExtractor;
}
public void setPrincipalExtractor(PrincipalExtractor principalExtractor) {
Assert.notNull(principalExtractor, "PrincipalExtractor must not be null");
this.principalExtractor = principalExtractor;
}
#Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
List<GrantedAuthority> authorities = this.authoritiesExtractor
.extractAuthorities(map);
OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
null, null, null, null);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
/**
* Return the principal that should be used for the token. The default implementation
* delegates to the {#link PrincipalExtractor}.
* #param map the source map
* #return the principal or {#literal "unknown"}
*/
protected Object getPrincipal(Map<String, Object> map) {
CustomPrincipal customPrincipal = new CustomPrincipal();
if( map.containsKey("principal") ) {
Map<String, Object> principalMap = (Map<String, Object>) map.get("principal");
customPrincipal.setEmail((String) principalMap.get("email"));
}
//and so on..
return customPrincipal;
/*
Object principal = this.principalExtractor.extractPrincipal(map);
return (principal == null ? "unknown" : principal);
*/
}
#Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
#SuppressWarnings({ "unchecked" })
private Map<String, Object> getMap(String path, String accessToken) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting user info from: " + path);
}
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
.getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
accessToken);
token.setTokenType(this.tokenType);
restTemplate.getOAuth2ClientContext().setAccessToken(token);
}
return restTemplate.getForEntity(path, Map.class).getBody();
}
catch (Exception ex) {
this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
+ ex.getMessage());
return Collections.<String, Object>singletonMap("error",
"Could not fetch user details");
}
}
}
A Custom PrincipalExtractor:
public class CustomPrincipalExtractor implements PrincipalExtractor {
private static final String[] PRINCIPAL_KEYS = new String[] {
"user", "username", "principal",
"userid", "user_id",
"login", "id",
"name", "uuid",
"email"};
#Override
public Object extractPrincipal(Map<String, Object> map) {
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
return map.get(key);
}
}
return null;
}
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setForcePrincipalAsString(false);
return daoAuthenticationProvider;
}
}
In your #Configuration file define a bean like this one
#Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
And in the Resource Server Configuration:
#Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(myUserInfoTokenServices());
}
//etc....
If everything is set correctly you can do something like this in your controller:
String userEmail = ((CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail();
Hope this helps.
A Map representation of the JSON object returned by the userdetails endpoint is available from the Authentication object that represents the Principal:
Map<String, Object> details = (Map<String,Object>)oauth2.getUserAuthentication().getDetails();
If you want to capture it for logging, storage or cacheing I'd recommend capturing it by implementing an ApplicationListener. For example:
#Component
public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {
private Logger log = LoggerFactory.getLogger(this.getClass());
#Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
log.debug("Authentication class: "+auth.getClass().toString());
if(auth instanceof OAuth2Authentication){
OAuth2Authentication oauth2 = (OAuth2Authentication)auth;
#SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>)oauth2.getUserAuthentication().getDetails();
log.info("User {} logged in: {}", oauth2.getName(), details);
log.info("User {} has authorities {} ", oauth2.getName(), oauth2.getAuthorities());
} else {
log.warn("User authenticated by a non OAuth2 mechanism. Class is "+auth.getClass());
}
}
}
If you specifically want to customize the extraction of the principal from the JSON or the authorities then you could implement org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor and/ org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor respectively.
Then, in a #Configuration class you would expose your implementations as beans:
#Bean
public PrincipalExtractor merckPrincipalExtractor() {
return new MyPrincipalExtractor();
}
#Bean
public AuthoritiesExtractor merckAuthoritiesExtractor() {
return new MyAuthoritiesExtractor();
}
You can use JWT tokens. You won't need datastore where all user information is stored instead you can encode additional information into the token itself. When token is decoded you app will be able to access all this information using Principal object
We retrieve it from the SecurityContextHolder's getContext method, which is static, and hence can be retrieved from anywhere.
// this is userAuthentication's principal
Map<?, ?> getUserAuthenticationFromSecurityContextHolder() {
Map<?, ?> userAuthentication = new HashMap<>();
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2Authentication)) {
return userAuthentication;
}
OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication;
Authentication userauthentication = oauth2Authentication.getUserAuthentication();
if (userauthentication == null) {
return userAuthentication;
}
Map<?, ?> details = (HashMap<?, ?>) userauthentication.getDetails(); //this effect in the new RW OAUTH2 userAuthentication
Object principal = details.containsKey("principal") ? details.get("principal") : userAuthentication; //this should be effect in the common OAUTH2 userAuthentication
if (!(principal instanceof Map)) {
return userAuthentication;
}
userAuthentication = (Map<?, ?>) principal;
} catch (Exception e) {
logger.error("Got exception while trying to obtain user info from security context.", e);
}
return userAuthentication;
}