web-dev-qa-db-fra.com

Intégrez Spring Security OAuth2 et Spring Social

Je travaille avec une application Spring Boot + Spring Security OAuth2 qui, je crois, a été inspirée par des exemples de Dave Syer. L'application est configurée pour être un serveur d'autorisation OAuth2, avec un seul client public utilisant le flux d'informations d'identification du mot de passe du propriétaire de la ressource. Un jeton réussi est configuré pour être un JWT.

Le client public Angular envoie une requête POST à/oauth/token avec un en-tête d'authentification de base contenant l'ID et le secret du client (c'était le moyen le plus simple d'obtenir le client doit s'authentifier, même si le secret n'est pas privé.) Le corps de la demande contient le nom d'utilisateur, le mot de passe et le type d'autorisation "mot de passe".

En plus d'être un serveur d'authentification, l'application est un serveur de ressources RESTful pour les utilisateurs, les équipes et les organisations.

J'essaie d'ajouter un flux d'authentification SSO supplémentaire à l'aide de Spring Social. J'ai Spring Social configuré pour s'authentifier via des fournisseurs externes via/auth/[fournisseur]; cependant, les requêtes suivantes n'ont plus le SecurityContext correctement défini. Peut-être, Spring Security OAuth serveur ou client remplace le SecurityContext?

Si je peux obtenir le SecurityContext correctement défini après le flux social de printemps, j'ai un nouveau TokenGranter qui autorise un nouveau type de subvention de "social" qui vérifierait le SecurityContextHolder pour l'utilisateur pré-authentifié.

Je suis intéressé à la fois par une solution à mon problème spécifique avec SecurityContext (je pense que c'est un problème avec Spring OAuth + intégration sociale), ou une approche différente pour l'authentification auprès de fournisseurs externes et l'obtention d'un JWT valide de notre propre serveur d'authentification.

Merci!

32
Andrew Ferk

J'ai eu un problème similaire sur une application Web générée par JHipster . J'ai finalement décidé d'utiliser l'option SocialAuthenticationFilter de Spring Social (via SpringSocialConfigurer). Après une connexion sociale réussie, le serveur génère et renvoie automatiquement le "propre" jeton d'accès via la redirection vers l'application cliente.

Voici mon essai:

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter implements EnvironmentAware {

    //...

    @Inject
    private AuthorizationServerTokenServices authTokenServices;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        SpringSocialConfigurer socialCfg = new SpringSocialConfigurer();
        socialCfg
            .addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
                @SuppressWarnings("unchecked")
                public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
                    filter.setAuthenticationSuccessHandler(
                            new SocialAuthenticationSuccessHandler(
                                    authTokenServices,
                                    YOUR_APP_CLIENT_ID
                            )
                        );
                    return filter;
                }
            });

        http
            //... lots of other configuration ...
            .apply(socialCfg);
    }        
}

Et la classe SocialAuthenticationSuccessHandler:

public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    public static final String REDIRECT_PATH_BASE = "/#/login";
    public static final String FIELD_TOKEN = "access_token";
    public static final String FIELD_EXPIRATION_SECS = "expires_in";

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final AuthorizationServerTokenServices authTokenServices;
    private final String localClientId;

    public SocialAuthenticationSuccessHandler(AuthorizationServerTokenServices authTokenServices, String localClientId){
        this.authTokenServices = authTokenServices;
        this.localClientId = localClientId;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
                    throws IOException, ServletException {
        log.debug("Social user authenticated: " + authentication.getPrincipal() + ", generating and sending local auth");
        OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(authentication)); //Automatically checks validity
        String redirectUrl = new StringBuilder(REDIRECT_PATH_BASE)
            .append("?").append(FIELD_TOKEN).append("=")
            .append(encode(oauth2Token.getValue()))
            .append("&").append(FIELD_EXPIRATION_SECS).append("=")
            .append(oauth2Token.getExpiresIn())
            .toString();
        log.debug("Sending redirection to " + redirectUrl);
        response.sendRedirect(redirectUrl);
    }

    private OAuth2Authentication convertAuthentication(Authentication authentication) {
        OAuth2Request request = new OAuth2Request(null, localClientId, null, true, null,
                null, null, null, null);
        return new OAuth2Authentication(request,
                //Other option: new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), "N/A", authorities)
                new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A")
                );
    }

    private String encode(String in){
        String res = in;
        try {
            res = UriUtils.encode(in, GeneralConstants.ENCODING_UTF8);
        } catch(UnsupportedEncodingException e){
            log.error("ERROR: unsupported encoding: " + GeneralConstants.ENCODING_UTF8, e);
        }
        return res;
    }
}

De cette façon, votre application client recevra le jeton d'accès de votre application Web par redirection vers /#/login?access_token=my_access_token&expires_in=seconds_to_expiration, tant que vous définissez le REDIRECT_PATH_BASE dans SocialAuthenticationSuccessHandler.

J'espère que ça aide.

12
rbarriuso

Tout d'abord, je vous recommande fortement de vous éloigner de l'octroi de mot de passe pour un tel cas d'utilisation.
. stocker les mots de passe de vos utilisateurs dans le processus.

La subvention implicite a été créée exactement pour ce que vous faites.
L'utilisation d'un flux basé sur la redirection a l'avantage de laisser le mécanisme d'authentification au serveur d'autorisation, au lieu d'avoir chacune de vos applications en avoir un morceau: c'est principalement la définition de Authentification unique (SSO) .

Cela dit, votre question est étroitement liée à celle à laquelle je viens de répondre: Propre serveur Spring OAuth2 avec 3rdparty OAuth provider

Pour résumer la réponse:

En fin de compte, il s'agit de la façon dont votre serveur d'autorisation sécurise le AuthorizationEndpoint:/oauth/authorize. Comme votre serveur d'autorisation fonctionne, vous disposez déjà d'une classe de configuration étendant WebSecurityConfigurerAdapter qui gère la sécurité de/oauth/authorize avec formLogin. C'est là que vous devez intégrer des éléments sociaux.

Vous ne pouvez tout simplement pas utiliser une autorisation de mot de passe pour ce que vous essayez de réaliser, vous devez avoir votre client public redirigé vers le serveur d'autorisation. Le serveur d'autorisation sera ensuite redirigé vers la connexion sociale comme mécanisme de sécurité pour le /oauth/authorize point final.

5
Michael Técourt

Je commençais avec la bonne réponse ci-dessus ( https://stackoverflow.com/a/33963286/3351474 ) mais avec ma version de Spring Security (4.2.8.RELEASE) cela échoue. La raison en est que dans org.springframework.security.access.intercept.AbstractSecurityInterceptor#authenticateIfRequired le PreAuthenticatedAuthenticationToken de la réponse n'est pas authentifié. Certaines Autorités accordées doivent être approuvées. De plus, le partage du jeton dans un paramètre d'URL n'est pas bon, il doit toujours être caché dans une charge utile ou un en-tête HTTPs. Au lieu de cela, un modèle HTML est chargé et la valeur du jeton est insérée dans un ${token} champ d'espace réservé.

Voici la version révisée:

REMARQUE: Le UserDetails utilisé ici implémente org.springframework.security.core.userdetails.UserDetails

@Component
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private OAuth2TokenStore tokenStore;

    @Qualifier("tokenServices")
    @Autowired
    private AuthorizationServerTokenServices authTokenServices;

    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        IClient user = ((SocialUserDetails) authentication.getPrincipal()).getUser();
        // registration is not finished, forward the user, a marker interface 
        // IRegistration is used here, remove this if there no two step approach to 
        // create a user from a social network
        if (user instanceof IRegistration) {
            response.sendRedirect(subscriberRegistrationUrl + "/" + user.getId());
        }
        OAuth2AccessToken token = loginUser(user);
        // load a HTML template from the class path and replace the token placeholder within, the HTML should contain a redirect to the actual page, but must store the token in a safe place, e.g. for preventing CSRF in the `sessionStorage` JavaScript storage.
        String html = IOUtils.toString(getClass().getResourceAsStream("/html/socialLoginRedirect.html"));
        html = html.replace("${token}", token.getValue());
        response.getOutputStream().write(html.getBytes(StandardCharsets.UTF_8));
    }

    private OAuth2Authentication convertAuthentication(Authentication authentication) {
        OAuth2Request request = new OAuth2Request(null, authentication.getName(),
                authentication.getAuthorities(), true, null,
                null, null, null, null);
        // note here the passing of the authentication.getAuthorities()
        return new OAuth2Authentication(request,
                new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A",  authentication.getAuthorities())
        );
    }

    /**
     * Logs in a user.
     */
    public OAuth2AccessToken loginUser(IClient user) {
        SecurityContext securityContext = SecurityContextHolder.getContext();
        UserDetails userDetails = new UserDetails(user);
        Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A", userDetails.getAuthorities());
        securityContext.setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = convertAuthentication(authentication);
        // delete the token because the client id in the DB is calculated as hash of the username and client id (here also also identical to username), this would be identical to the
        // to an existing user. This existing one can come from a user registration or a previous user with the same name.
        // If a new entity with a different ID is used the stored token hash would differ and the the wrong token would be retrieved 
        tokenStore.deleteTokensForUserId(user.getUsername());
        OAuth2AccessToken oAuth2AccessToken = authTokenServices.createAccessToken(oAuth2Authentication);
        // the DB id of the created user is returned as additional data, can be 
        // removed if not needed
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(new HashMap<>());
        oAuth2AccessToken.getAdditionalInformation().put("userId", user.getId());
        return oAuth2AccessToken;
    }

}

Exemple socialLoginRedirect.html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Example App</title>
    <meta http-equiv="Refresh" content="0; url=/index.html#/home"/>
</head>
<script>
     window.sessionStorage.setItem('access_token', '${token}');
</script>
<body>
<p>Please follow <a href="/index.html#/home">this link</a>.</p>
</body>
</html>

Le câblage de configuration dans un WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(WebServiceConfig.class)
public class AuthenticationConfig extends WebSecurityConfigurerAdapter {

    @Value("${registrationUrl}")
    private String registrationUrl;

    @Autowired
    private SocialAuthenticationSuccessHandler socialAuthenticationSuccessHandler;

    @Value("${loginUrl}")
    private String loginUrl;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> permitAllUrls = new ArrayList<>();
        // permit social log in
        permitAllUrls.add("/auth/**");
        http.authorizeRequests().antMatchers(permitAllUrls.toArray(new String[0])).permitAll();

        SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
        springSocialConfigurer.signupUrl(registrationUrl);
        springSocialConfigurer.postFailureUrl(loginUrl);
        springSocialConfigurer
                .addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
                    @SuppressWarnings("unchecked")
                    public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
                        filter.setAuthenticationSuccessHandler(socialAuthenticationSuccessHandler);
                        return filter;
                    }
                });
        http.apply(springSocialConfigurer);

        http.logout().disable().csrf().disable();

        http.requiresChannel().anyRequest().requiresSecure();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
1
k_o_

J'ai implémenté spring oauth2 pour sécuriser mes services de repos et ajouter en plus une connexion sociale et une inscription implicite pour la première connexion. pour l'utilisateur utilisateur, vous pouvez générer le jeton en utilisant uniquement le nom d'utilisateur et le mot de passe pour générer le jeton pour l'utilisateur social. pour cela, vous devez implémenter le filtre qui interceptera votre demande/oauth/token avant le traitement. ici, si vous souhaitez générer le token pour l'utilisateur social, passez le nom d'utilisateur et le token facebook, ici vous pouvez utiliser le token facebook comme mot de passe et générer également le token pour l'utilisateur facebook. si le token facebook est mis à jour, vous devez également écrire un déclencheur db pour mettre à jour votre token dans la table utilisateur .... peut-être que cela vous aidera

0
Amit Patel