web-dev-qa-db-fra.com

L'exemple officiel de sécurité Spring oauth2 ne fonctionne pas en raison du conflit des cookies (mécanisme de code d'autorisation)

Selon le tutoriel Spring Boot et OAuth2

J'ai la structure de projet suivante:

enter image description here

Et le code source suivant:

SocialApplication.class:

@SpringBootApplication
@RestController
@EnableOAuth2Client
@EnableAuthorizationServer
@Order(200)
public class SocialApplication extends WebSecurityConfigurerAdapter {

    @Autowired
    OAuth2ClientContext oauth2ClientContext;

    @RequestMapping({ "/user", "/me" })
    public Map<String, String> user(Principal principal) {
        Map<String, String> map = new LinkedHashMap<>();
        map.put("name", principal.getName());
        return map;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.antMatcher("/**").authorizeRequests().antMatchers("/", "/login**", "/webjars/**").permitAll().anyRequest()
                .authenticated().and().exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/")).and().logout()
                .logoutSuccessUrl("/").permitAll().and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
                .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class);
        // @formatter:on
    }

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            // @formatter:off
            http.antMatcher("/me").authorizeRequests().anyRequest().authenticated();
            // @formatter:on
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(SocialApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
        FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>();
        registration.setFilter(filter);
        registration.setOrder(-100);
        return registration;
    }

    @Bean
    @ConfigurationProperties("github")
    public ClientResources github() {
        return new ClientResources();
    }

    @Bean
    @ConfigurationProperties("facebook")
    public ClientResources facebook() {
        return new ClientResources();
    }

    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(facebook(), "/login/facebook"));
        filters.add(ssoFilter(github(), "/login/github"));
        filter.setFilters(filters);
        return filter;
    }

    private Filter ssoFilter(ClientResources client, String path) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
                path);
        OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
        filter.setRestTemplate(template);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(
                client.getResource().getUserInfoUri(),
                client.getClient().getClientId());
        tokenServices.setRestTemplate(template);
        filter.setTokenServices(new UserInfoTokenServices(
                client.getResource().getUserInfoUri(),
                client.getClient().getClientId()));
        return filter;
    }

}

class ClientResources {

    @NestedConfigurationProperty
    private AuthorizationCodeResourceDetails client = new AuthorizationCodeResourceDetails();

    @NestedConfigurationProperty
    private ResourceServerProperties resource = new ResourceServerProperties();

    public AuthorizationCodeResourceDetails getClient() {
        return client;
    }

    public ResourceServerProperties getResource() {
        return resource;
    }
}

index.html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <title>Demo</title>
    <meta name="description" content=""/>
    <meta name="viewport" content="width=device-width"/>
    <base href="/"/>
    <link rel="stylesheet" type="text/css"
          href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script>
    <script type="text/javascript"
            src="/webjars/bootstrap/js/bootstrap.min.js"></script>
</head>
<body>
<h1>Login</h1>
<div class="container unauthenticated">
    With Facebook: <a href="/login/facebook">click here</a>
</div>
<div class="container authenticated" style="display: none">
    Logged in as: <span id="user"></span>
    <div>
        <button onClick="logout()" class="btn btn-primary">Logout</button>
    </div>
</div>
<script type="text/javascript"
        src="/webjars/js-cookie/js.cookie.js"></script>
<script type="text/javascript">
    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (settings.type == 'POST' || settings.type == 'PUT'
                || settings.type == 'DELETE') {
                if (!(/^http:.*/.test(settings.url) || /^https:.*/
                        .test(settings.url))) {
                    // Only send the token to relative URLs i.e. locally.
                    xhr.setRequestHeader("X-XSRF-TOKEN",
                        Cookies.get('XSRF-TOKEN'));
                }
            }
        }
    });
    $.get("/user", function (data) {
        $("#user").html(data.userAuthentication.details.name);
        $(".unauthenticated").hide();
        $(".authenticated").show();
    });
    var logout = function () {
        $.post("/logout", function () {
            $("#user").html('');
            $(".unauthenticated").show();
            $(".authenticated").hide();
        });
        return true;
    }
</script>
</body>
</html>

application.yml:

server:
  port: 8080
security:
  oauth2:
    client:
      client-id: acme
      client-secret: acmesecret
      scope: read,write
      auto-approve-scopes: '.*'

facebook:
  client:
    clientId: 233668646673605
    clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me
github:
  client:
    clientId: bd1c0a783ccdd1c9b9e4
    clientSecret: 1a9030fbca47a5b2c28e92f19050bb77824b5ad1
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user

logging:
  level:
    org.springframework.security: DEBUG

Mais quand j'ouvre le navigateur et que j'essaye de frapper http://localhost:8080

Dans la console du navigateur, je vois:

(index):44 Uncaught TypeError: Cannot read property 'details' of undefined
    at Object.success ((index):44)
    at j (jquery.js:3073)
    at Object.fireWith [as resolveWith] (jquery.js:3185)
    at x (jquery.js:8251)
    at XMLHttpRequest.<anonymous> (jquery.js:8598)

dans du code:

$.get("/user", function (data) {
        $("#user").html(data.userAuthentication.details.name);
        $(".unauthenticated").hide();
        $(".authenticated").show();
    });

Cela se produit parce que /user réponse avec code d'état 302 et rappel js essayer d'analyser le résultat de localhost:8080:

enter image description here

Je ne comprends pas pourquoi cette redirection se produit. Pouvez-vous expliquer ce comportement et aider à le corriger?

MISE À JOUR

J'ai pris ce code de https://github.com/spring-guides/tut-spring-boot-oauth2

important:

Il ne se reproduit qu'après le démarrage de l'application client.

P.S.

Comment reproduire:

Pour tester les nouvelles fonctionnalités, vous pouvez simplement exécuter les deux applications et visiter localhost: 9999/client dans votre navigateur. L'application cliente sera redirigée vers le serveur d'autorisation local, qui donne ensuite à l'utilisateur le choix habituel d'authentification avec Facebook ou Github. Une fois ce contrôle terminé, le client de test retourne, le jeton d'accès local est accordé et l'authentification est terminée (vous devriez voir un message "Bonjour" dans votre navigateur). Si vous êtes déjà authentifié avec Github ou Facebook, vous ne remarquerez peut-être même pas l'authentification à distance

RÉPONDRE:

https://stackoverflow.com/a/50349078/26743

29
gstackoverflow

Enfin, j'ai trouvé le problème. Je vois ce comportement dû au fait que les cookies s'affrontent pour le client et le serveur si vous démarrez les deux applications sur localhost.

Cela se produit en raison du fait que la propriété est incorrecte pour le contexte.

Donc, pour corriger l'application, vous devez remplacer:

server:
  context-path: /client

avec

server:  
  servlet:
    context-path: /client

P.S.

J'ai créé un problème sur github:

https://github.com/spring-guides/tut-spring-boot-oauth2/issues/8

et fait la demande de pull:

https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81

P.S.2

Enfin, ma demande de pull a été fusionnée: https://github.com/spring-guides/tut-spring-boot-oauth2/pull/81

6
gstackoverflow

Mise à jour: 15 mai 2018

Comme vous avez déjà trouvé la solution, le problème se produit car le JSESSIONID est écrasé

Session ID replaced

Mise à jour: 10 mai 2018

Eh bien, votre persévérance avec la 3e prime a finalement porté ses fruits. J'ai commencé à creuser ce qui était différent entre les deux exemples que vous aviez dans le repo

Si vous regardez le repo manual et /user mappage

@RequestMapping("/user")
public Principal user(Principal principal) {
    return principal;
}

Comme vous pouvez le voir, vous retournez le principal ici, vous obtenez plus de détails du même objet. Maintenant, dans votre code que vous exécutez à partir de auth-server dossier

@RequestMapping({ "/user", "/me" })
public Map<String, String> user(Principal principal) {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("name", principal.getName());
    return map;
}

Comme vous pouvez le voir, vous n'avez renvoyé que le name dans le /user le mappage et votre logique d'interface utilisateur s'exécute ci-dessous

$.get("/user", function(data) {
    $("#user").html(data.userAuthentication.details.name);
    $(".unauthenticated").hide();
    $(".authenticated").show();
});

La réponse json est donc revenue de /user api devrait avoir userAuthentication.details.name par l'interface utilisateur n'a pas ces détails. Maintenant, si j'ai mis à jour la méthode comme ci-dessous dans le même projet

@RequestMapping({"/user", "/me"})
public Map<String, Object> user(Principal principal) {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("name", principal.getName());
    OAuth2Authentication user = (OAuth2Authentication) principal;
    map.put("userAuthentication", new HashMap<String, Object>(){{
       put("details", user.getUserAuthentication().getDetails());
    }});
    return map;
}

Et puis vérifiez l'application, ça marche

OAuth Success

Réponse originale

Donc, le problème que vous exécutez un mauvais projet à partir du référentiel. Le projet que vous exécutez est auth-server qui sert à lancer votre propre serveur oauth. Le projet que vous devez exécuter se trouve dans le dossier manual.

Maintenant, si vous regardez le code ci-dessous

OAuth2ClientAuthenticationProcessingFilter facebookFilter = new OAuth2ClientAuthenticationProcessingFilter(
        "/login/facebook");
OAuth2RestTemplate facebookTemplate = new OAuth2RestTemplate(facebook(), oauth2ClientContext);
facebookFilter.setRestTemplate(facebookTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(facebookResource().getUserInfoUri(),
        facebook().getClientId());
tokenServices.setRestTemplate(facebookTemplate);
facebookFilter.setTokenServices(
        new UserInfoTokenServices(facebookResource().getUserInfoUri(), facebook().getClientId()));
return facebookFilter;

Et le code réel que vous exécutez a

private Filter ssoFilter(ClientResources client, String path) {
    OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
            path);
    OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), oauth2ClientContext);
    filter.setRestTemplate(template);
    UserInfoTokenServices tokenServices = new UserInfoTokenServices(
            client.getResource().getUserInfoUri(), client.getClient().getClientId());
    tokenServices.setRestTemplate(template);
    filter.setTokenServices(tokenServices);
    return filter;
}

Dans votre courant, le userdetails du facebook n'est pas collecté. C'est pourquoi vous voyez une erreur

Error

Parce que lorsque vous vous êtes connecté, l'utilisateur n'a pas collecté ses informations utilisateur. Donc, lorsque vous accédez aux détails, ce n'est pas là. Et donc vous obtenez une erreur

Si vous exécutez le dossier manual correct, cela fonctionne

Working

15
Tarun Lalwani

Je vois deux requêtes dans votre message.

N -

(index):44 Uncaught TypeError: Cannot read property 'details' of undefined

Cela se produisait parce que vous exécutiez peut-être un mauvais projet (c'est-à-dire un serveur d'authentification) qui a un bogue. Le dépôt contient d'autres projets similaires également sans bogue. Si vous exécutez le projet manuel ou github cette erreur n'apparaîtra pas. Dans ces projets, le code javascript gère correctement les données renvoyées par le serveur après l'authentification.

DEUX -

/userréponse avec code d'état 302:

Pour comprendre pourquoi cela se produit, voyons la configuration de sécurité de cette application.

Les points finaux "/", "/login**" et "/logout" sont accessibles à tous. Tous les autres points finaux, y compris "/user" nécessite une authentification car vous avez utilisé

.anyRequest().authenticated().and().exceptionHandling()
                .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/"))

Ainsi, toute demande non authentifiée sera redirigée vers le point d'entrée d'authentification, c'est-à-dire "/", demandant à l'utilisateur une authentification. Cela ne dépend pas du démarrage ou non de votre application client. Tant que la demande n'est pas authentifiée, elle sera redirigée vers "/". C'est pourquoi le contrôleur de ressort répond avec le statut 302. Une fois que vous vous êtes authentifié avec facebook ou github, les demandes suivantes à "/user" le point final répondra avec succès avec 200.

ET SUIVANT -

Le point final "/me" dans votre application est sécurisé en tant que ressource sécurisée avec @EnableResourceServer. Étant donné que ResourceServerConfiguration a une priorité plus élevée (ordonnée 3 par défaut) que WebSecurityConfigurerAdapter (par défaut 100, de toute façon il a déjà ordonné explicitement inférieur à 3 avec l'annotation @Order dans le code), donc ResourceServerConfiguration s'appliquera pour cela point final. Cela signifie que si la demande n'est pas authentifiée, elle ne sera pas redirigée vers le point d'entrée d'authentification, elle retournera plutôt une réponse 401. Une fois que vous êtes authentifié, il répondra avec succès à 200.

J'espère que cela clarifiera toutes vos questions.

PDATE - pour répondre à votre question

Le lien vers le référentiel que vous avez fourni dans votre article contient de nombreux projets. Les projets auth-server, manual et github sont tous similaires (fournissent la même fonctionnalité, c'est-à-dire l'authentification avec facebook et github). Seulement le index.html in auth-server projet a un bug. Si vous corrigez ce bug qui est remplacer

$("#user").html(data.userAuthentication.details.name);

avec

$("#user").html(data.name);

il fonctionnera également très bien. Les trois projets donneront le même résultat.

6
vsoni