How to get custom user info from OAuth2 authorization server /user endpoint How to get custom user info from OAuth2 authorization server /user endpoint spring spring

How to get custom user info from OAuth2 authorization server /user endpoint


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:

@Autowiredprivate ResourceServerProperties sso;@Beanpublic 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:

@Configurationpublic 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.