web-dev-qa-db-fra.com

Comment puis-je lire tous les utilisateurs à l'aide de keycloak et spring?

J'utilise keycloak 3.4 et spring boot pour développer une application web. J'utilise Active Directory en tant que fédération d'utilisateurs pour récupérer toutes les informations des utilisateurs.

Mais pour utiliser ces informations dans mon application web, je pense que je dois les enregistrer dans la base de données "local-webapp".

Ainsi, une fois les utilisateurs connectés, comment puis-je les enregistrer dans ma base de données?

Je pense à un scénario comme: "J'ai un objet A qui se réfère à l'utilisateur B, donc je dois mettre une relation entre eux . J'ajoute donc une clé étrangère. "

Dans ce cas, je dois avoir l'utilisateur sur ma base de données. non?

[~ # ~] modifier [~ # ~]

Pour éviter de sauvegarder tous les utilisateurs de ma base de données, j'essaie d'utiliser l'API administrateur, j'ai donc ajouté le code suivant dans un contrôleur.

J'ai également créé un autre client appelé Test pour obtenir tous les utilisateurs, de cette façon je peux utiliser client-id et client-secret. Ou existe-t-il un moyen d'utiliser le JWT pour utiliser l'api d'administration?

Le client:

     Keycloak keycloak2 = KeycloakBuilder.builder()
                         .serverUrl("http://localhost:8080/auth/admin/realms/MYREALM/users")
                         .realm("MYREALMM")
                         .username("u.user")
                         .password("password")
                         .clientId("Test")
                         .clientSecret("cade3034-6ee1-4b18-8627-2df9a315cf3d")
                         .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
                         .build();

 RealmRepresentation realm2 = keycloak2.realm("MYREALMM").toRepresentation();

l'erreur est:

2018-02-05 12:33:06.638 ERROR 16975 --- [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is Java.lang.Error: Unresolved compilation problem: 
    The method realm(String) is undefined for the type AccessTokenResponse
] with root cause

Java.lang.Error: Unresolved compilation problem: 
    The method realm(String) is undefined for the type AccessTokenResponse

Où est-ce que je fais mal?

EDIT 2

J'ai également essayé ceci:

@Autowired
private HttpServletRequest request;

public ResponseEntity listUsers() {
    KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) request.getUserPrincipal();        
    KeycloakPrincipal principal=(KeycloakPrincipal)token.getPrincipal();
    KeycloakSecurityContext session = principal.getKeycloakSecurityContext();

    Keycloak keycloak = KeycloakBuilder.builder()
                                        .serverUrl("http://localhost:8080/auth")
                                        .realm("MYREALMM")
                                        .authorization(session.getToken().getAuthorization().toString())
                                        .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
                                        .build();

    RealmResource r = keycloak.realm("MYREALMM");
    List<org.keycloak.representations.idm.UserRepresentation> list = keycloak.realm("MYREALMM").users().list();
    return ResponseEntity.ok(list);

mais l'autorisation est toujours null. Pourquoi?

EDIT 3 Vous trouverez ci-dessous ma configuration de sécurité de printemps:

    @Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
         super.configure(http);

        http.httpBasic().disable();
        http
        .csrf().disable()
        .authorizeRequests()
            .antMatchers("/webjars/**").permitAll()
            .antMatchers("/resources/**").permitAll()
            .anyRequest().authenticated()
        .and()
        .logout()
            .logoutUrl("/logout")
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
            .permitAll()
            .logoutSuccessUrl("/")
            .invalidateHttpSession(true);
    }

      @Autowired
        public KeycloakClientRequestFactory keycloakClientRequestFactory;

        @Bean
        public KeycloakRestTemplate keycloakRestTemplate() {
            return new KeycloakRestTemplate(keycloakClientRequestFactory);
        }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {

        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        SimpleAuthorityMapper simpleAuthorityMapper = new SimpleAuthorityMapper();
        simpleAuthorityMapper.setPrefix("ROLE_");
        simpleAuthorityMapper.setConvertToUpperCase(true);
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(simpleAuthorityMapper);
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
           .ignoring()
           .antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
    }

     @Bean
     @Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
     public AccessToken accessToken() {
         HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
         return ((KeycloakSecurityContext) ((KeycloakAuthenticationToken) request.getUserPrincipal()).getCredentials()).getToken();
     }

}

EDIT 4

Ce sont les propriétés à l'intérieur du applicatoin.properties

#######################################
#             KEYCLOAK                #
#######################################

keycloak.auth-server-url=http://localhost:8181/auth
keycloak.realm=My Realm 
keycloak.ssl-required=external
keycloak.resource=AuthServer
keycloak.credentials.jwt.client-key-password=keystorePwd
keycloak.credentials.jwt.client-keystore-file=keystore.jks
keycloak.credentials.jwt.client-keystore-password=keystorePwd
keycloak.credentials.jwt.alias=AuthServer
keycloak.credentials.jwt.token-expiration=10
keycloak.credentials.jwt.client-keystore-type=JKS
keycloak.use-resource-role-mappings=true
keycloak.confidential-port=0
keycloak.principal-attribute=preferred_username

MODIFIER 5.

Voici ma configuration keycocloak: enter image description here

l'utilisateur que j'utilise pour me connecter avec l'autorisation de voir l'utilisateur: enter image description here

EDIT 6

Voici la clé du formulaire de journal après avoir activé la journalisation:

2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler       : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] .k.a.t.AbstractAuthenticatedActionsValve : AuthenticatedActionsValve.invoke /utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.274 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler       : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.275 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.adapters.PreAuthActionsHandler       : adminRequest http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler        : AuthenticatedActionsValve.invoke http://localhost:8080/utente/prova4
2018-02-12 08:31:00.276 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.AuthenticatedActionsHandler        : Policy enforcement is disabled.
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate      : Created GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users"
2018-02-12 08:31:10.580 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate      : Setting request Accept header to [application/json, application/*+json]
2018-02-12 08:31:10.592 3DEBUG 5802 --- [nio-8080-exec-1] o.k.a.s.client.KeycloakRestTemplate      : GET request for "http://localhost:8181/auth/admin/realms/My%20Realm%20name/users" resulted in 401 (Unauthorized); invoking error handler
2018-02-12 08:31:10.595 ERROR 5802 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.HttpClientErrorException: 401 Unauthorized] with root cause

org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.Java:85) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.Java:707) ~[spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
7
Teo

Pour accéder à la liste complète des utilisateurs, vous devez vérifier que l'utilisateur connecté contient au moins le rôle view-users Du client realm-management, Voir cette réponse j'ai écrit il y a quelque temps. Une fois que l'utilisateur a ce rôle, le JWT qu'il récupère le cointainera.

Comme je peux déduire de vos commentaires, vous semblez manquer de bases sur l'en-tête Authorization. Une fois que l'utilisateur est connecté, il obtient le JWT signé de keycloak, afin que chaque client du domaine puisse lui faire confiance, sans avoir à demander à Keycloak. Ce JWT contient le jeton d'accès, qui sera plus tard requis dans l'en-tête Authorization pour chaque demande de l'utilisateur, préfixé par le mot clé Bearer (voir Token- Authentification basée dans https://auth0.com/blog/cookies-vs-tokens-definitive-guide/ ).

Ainsi, lorsque l'utilisateur fait la demande à votre application afin d'afficher la liste des utilisateurs, son jeton d'accès contenant le rôle view-users Va déjà dans les en-têtes de demande. Au lieu de devoir l'analyser manuellement, créez vous-même une autre demande pour accéder au point de terminaison utilisateur Keycloak et le joindre (comme vous semblez le faire avec KeycloakBuilder), l'adaptateur Keycloak Spring Security fournit déjà un KeycloakRestTemplate classe, qui est capable d'exécuter une requête vers un autre service pour l'utilisateur actuel:

SecurityConfig.Java

@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    ...

    @Autowired
    public KeycloakClientRequestFactory keycloakClientRequestFactory;

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public KeycloakRestTemplate keycloakRestTemplate() {
        return new KeycloakRestTemplate(keycloakClientRequestFactory);
    }

    ...
}

Notez que la portée du modèle est PROTOTYPE, donc Spring utilisera une instance différente pour chacune des demandes en cours.

Ensuite, câblez automatiquement ce modèle et utilisez-le pour faire des demandes:

@Service
public class UserRetrievalService{

    @Autowired
    private KeycloakRestTemplate keycloakRestTemplate;

    public List<User> getUsers() {
        ResponseEntity<User[]> response = keycloakRestTemplate.getForEntity(keycloakUserListEndpoint, User[].class);
        return Arrays.asList(response.getBody());
    }

}

Vous devrez implémenter votre propre classe User qui correspond à la réponse JSON retournée par le serveur keycloak.

Notez que, lorsque l'utilisateur n'est pas autorisé à accéder à la liste, un code de réponse 403 est renvoyé par le serveur Keycloak. Vous pouvez même le refuser avant vous-même, en utilisant des annotations comme: @PreAuthorize("hasRole('VIEW_USERS')").

Enfin et surtout, je pense que la réponse de @ dchrzascik est bien indiquée. Pour résumer, je dirais qu'il existe en fait une autre façon d'éviter de récupérer la liste complète des utilisateurs du serveur keycloak à chaque fois ou d'avoir vos utilisateurs stockés dans votre base de données d'application: vous pouvez réellement les mettre en cache, de sorte que vous pouvez mettre à jour ce cache si vous gérer les utilisateurs depuis votre application.


[~ # ~] modifier [~ # ~]

J'ai implémenté un exemple de projet pour montrer comment obtenir la liste complète des utilisateurs, téléchargée sur Github . Il est configuré pour un client confidentiel (lors de l'utilisation d'un client public, le secret doit être supprimé du fichier application.properties).

Voir aussi:

9
Xtreme Biker

Je suggère de vérifier si vous avez vraiment besoin d'avoir votre propre magasin d'utilisateurs. Vous devez relayer uniquement sur la fédération des utilisateurs de Keycloak pour éviter la duplication des données et donc éviter les problèmes qui en découlent. Entre autres, Keycloak est responsable de la gestion des utilisateurs et vous devez le laisser faire son travail.

Puisque vous utilisez OIDC, vous bénéficiez de deux avantages:

  1. Dans le jeton d'identité que vous obtenez sous forme de JWT, vous avez un champ "sous". Ce champ identifie de manière unique un utilisateur. De la spécification OpenID Connect :

    OBLIGATOIRE. Identifiant du sujet. Un identifiant localement unique et jamais réaffecté au sein de l'émetteur pour l'utilisateur final, qui est destiné à être consommé par le client, par exemple, 24400320 ou AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. Il NE DOIT PAS dépasser 255 ASCII caractères. La sous-valeur est une chaîne sensible à la casse.

    Dans keycloak, "sub" est juste un UUID. Vous pouvez utiliser ce champ pour corréler votre "objet A" avec "l'utilisateur B". Dans votre base de données, ce ne serait qu'une colonne régulière, pas une clé étrangère.

    En Java, vous pouvez accéder à ces données JWT en utilisant contexte de sécurité . Vous pouvez également consulter démarrage rapide authz-springboot de keycloak où il est montré comment vous pouvez accéder KeycloakSecurityContext - à partir de là, vous pouvez obtenir un IDToken qui a une méthode getSubject.

  2. Keycloak fournit Admin REST API qui a une ressource utilisateurs . Il s'agit d'API prise en charge par OIDC, vous devez donc être correctement authentifié. En utilisant cette API, vous pouvez effectuer des opérations sur les utilisateurs - Vous pouvez utiliser cette API directement ou en utilisant Java SDK: client administrateur keycloak .

    Dans ce scénario, vous devez utiliser le JWT que vous obtenez de l'utilisateur à la demande. En utilisant JWT, vous êtes sûr que quelqu'un qui fait une demande peut répertorier tous les utilisateurs de ce domaine. Par exemple, veuillez considérer le code suivant:

    @GetMapping("/users")
    public List<UserRepresentation> check(HttpServletRequest request){
        KeycloakSecurityContext context = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
    
        Keycloak keycloak = KeycloakBuilder.builder()
                                       .serverUrl("http://localhost:8080/auth")
                                       .realm("example")
                                       .authorization(context.getTokenString())
                                       .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build())
                                       .build();
    
       List<UserRepresentation> list = keycloak.realm("example").users().list();
    
       return list;
    }
    

    Dans ce cas, nous utilisons HttpServletRequest et le jeton qu'il contient. Nous pouvons obtenir les mêmes données en utilisant org.springframework.security.core.Authentication de la sécurité du printemps ou obtenir directement un en-tête d'autorisation. Le fait est que KeycloakBuilder attend une chaîne comme une "autorisation", pas un AccessToken - c'est la raison pour laquelle vous avez cette erreur.

    Veuillez garder à l'esprit que pour que cela fonctionne, l'utilisateur qui crée une demande doit avoir un rôle de "visualisation des utilisateurs" du client de "gestion de domaine". Vous pouvez lui attribuer ce rôle dans l'onglet "Correspondance des rôles" pour cet utilisateur ou un groupe auquel il appartient.

    De plus, vous devez être correctement authentifié pour bénéficier du contexte de sécurité, sinon vous obtiendrez un null. Exemple de classe de configuration de la clé de sécurité de printemps:

    @Configuration
    @EnableWebSecurity
    @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
    class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }
    
    @Bean
    public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
    
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
            .antMatchers("/api/users/*")
            .hasRole("admin")
            .anyRequest()
            .permitAll();
    }
    }
    
4
dchrzascik