web-dev-qa-db-fra.com

Comment obtenir des informations utilisateur personnalisées à partir du serveur d'autorisation OAuth2 / du point de terminaison utilisateur

J'ai un serveur de ressources configuré avec @EnableResourceServer annotation et fait référence au serveur d'autorisation via user-info-uri paramètre comme suit:

security:
  oauth2:
    resource:
      user-info-uri: http://localhost:9001/user


Le serveur d'autorisation/point de terminaison utilisateur renvoie une extension de org.springframework.security.core.userdetails.User qui a par exemple un courriel:

{  
   "password":null,
   "username":"myuser",
    ...
   "email":"[email protected]"
}


À chaque accès à un point de terminaison de serveur de ressources, Spring vérifie le jeton d'accès en arrière-plan en appelant le /user endpoint et il récupère en fait les informations utilisateur enrichies (qui contiennent par exemple des informations sur les e-mails, j'ai vérifié cela avec Wireshark).

La question est donc de savoir comment obtenir ces informations utilisateur personnalisées sans un deuxième appel explicite au serveur d'autorisation /user point final. Spring le stocke-t-il quelque part localement sur le serveur de ressources après l'autorisation ou quel est le meilleur moyen d'implémenter ce type de stockage d'informations utilisateur s'il n'y a rien de disponible?

18
Sergey Pauk

La solution est l'implémentation d'un UserInfoTokenServices personnalisé

https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/Java/org/springframework/boot/autoconfigure/security/oauth2/resource/ UserInfoTokenServices.Java

Fournissez simplement votre implémentation personnalisée en tant que bean et elle sera utilisée à la place de celle par défaut.

Dans cet UserInfoTokenServices, vous pouvez créer le principal comme vous le souhaitez.

Ce UserInfoTokenServices est utilisé pour extraire les UserDetails de la réponse du /usersendpoint de votre serveur d'autorisation. Comme vous pouvez le voir dans

private Object getPrincipal(Map<String, Object> map) {
    for (String key : PRINCIPAL_KEYS) {
        if (map.containsKey(key)) {
            return map.get(key);
        }
    }
    return "unknown";
}

Seules les propriétés spécifiées dans PRINCIPAL_KEYS sont extraits par défaut. Et c'est exactement votre problème. Vous devez extraire plus que le nom d'utilisateur ou le nom de votre propriété. Alors cherchez plus de clés.

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;
}

Câblage:

@Autowired
private ResourceServerProperties sso;

@Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
    return new MyUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}

!! MISE À JOUR avec Spring Boot 1.4 les choses deviennent plus faciles !!

Avec Spring Boot 1.4.0, PrincipalExtractor a été introduit. Cette classe doit être implémentée pour extraire un principal personnalisé (voir Spring Boot 1.4 Release Notes ).

20
Yannic Klem

Toutes les données sont déjà dans l'objet Principal, aucune seconde demande n'est nécessaire. Retournez uniquement ce dont vous avez besoin. J'utilise la méthode ci-dessous pour me connecter à Facebook:

@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;
}
5
user2802927

Dans le serveur de ressources, vous pouvez créer une classe CustomPrincipal comme ceci:

public class CustomPrincipal {

    public CustomPrincipal(){};

    private String email;

    //Getters and Setters
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

}

Implémentez un CustomUserInfoTokenServices comme ceci:

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");
        }
    }

}

Un PrincipalExtractor personnalisé:

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;
    }

}

Dans votre fichier @Configuration, définissez un bean comme celui-ci

@Bean
    public ResourceServerTokenServices myUserInfoTokenServices() {
        return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
    }

Et dans la configuration du serveur de ressources:

@Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Override
    public void configure(ResourceServerSecurityConfigurer config) {
        config.tokenServices(myUserInfoTokenServices());
    }

    //etc....

Si tout est correctement réglé, vous pouvez faire quelque chose comme ça dans votre contrôleur:

String userEmail = ((CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail();

J'espère que cela t'aides.

3
Paolo Mastinu

Vous pouvez utiliser des jetons JWT. Vous n'aurez pas besoin d'une banque de données où toutes les informations utilisateur sont stockées, mais vous pouvez encoder des informations supplémentaires dans le jeton lui-même. Lorsque le jeton est décodé, votre application pourra accéder à toutes ces informations à l'aide de l'objet Principal

1
vladsfl

Nous le récupérons à partir de la méthode getContext de SecurityContextHolder, qui est statique, et peut donc être récupérée de n'importe où.

// 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;
}
1
Jose Martinez

Une représentation Map de l'objet JSON retourné par le point de terminaison userdetails est disponible à partir de l'objet Authentication qui représente le principal:

Map<String, Object> details = (Map<String,Object>)oauth2.getUserAuthentication().getDetails();

Si vous souhaitez le capturer pour la journalisation, le stockage ou la mise en cache, je vous recommande de le capturer en implémentant un ApplicationListener. Par exemple:

@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());
    }

  }
}

Si vous souhaitez spécifiquement personnaliser l'extraction du principal à partir du JSON ou des autorités, vous pouvez implémenter org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor et/ org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor respectivement.

Ensuite, dans un @Configuration classe, vous exposeriez vos implémentations en tant que beans:

@Bean
public PrincipalExtractor merckPrincipalExtractor() {
        return new MyPrincipalExtractor();
}

@Bean 
public AuthoritiesExtractor merckAuthoritiesExtractor() {
        return new MyAuthoritiesExtractor(); 
}
0
Mark