Utilisation de JWT avec Spring Security OAuth

1. Vue d'ensemble

Dans ce didacticiel, nous verrons comment obtenir notre implémentation Spring Security OAuth2 pour utiliser les jetons Web JSON.

Nous continuons également à nous appuyer sur l'article Spring REST API + OAuth2 + Angular dans cette série OAuth.

2. Le serveur d'autorisation OAuth2

Auparavant, la pile Spring Security OAuth offrait la possibilité de configurer un serveur d'autorisation en tant qu'application Spring. Nous avons ensuite dû le configurer pour utiliser JwtTokenStore afin que nous puissions utiliser des jetons JWT.

Cependant, la pile OAuth a été déconseillée par Spring et nous allons maintenant utiliser Keycloak comme serveur d'autorisation.

Donc, cette fois, nous allons configurer notre serveur d'autorisation en tant que serveur Keycloak intégré dans une application Spring Boot . Il émet des jetons JWT par défaut, il n'y a donc pas besoin d'une autre configuration à cet égard.

3. Serveur de ressources

Voyons maintenant comment configurer notre serveur de ressources pour utiliser JWT.

Nous allons le faire dans un fichier application.yml :

server: port: 8081 servlet: context-path: /resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

Les JWT incluent toutes les informations contenues dans le jeton. Le serveur de ressources doit donc vérifier la signature du jeton pour s'assurer que les données n'ont pas été modifiées. Le JWK-set-uri propriété contient la clé publique que le serveur peut utiliser à cet effet .

La propriété issuer-uri pointe vers l'URI du serveur d'autorisation de base, qui peut également être utilisé pour vérifier la revendication iss , comme mesure de sécurité supplémentaire.

En outre, si la propriété jwk-set-uri n'est pas définie, le serveur de ressources tentera d'utiliser l' émetteur-ui pour déterminer l'emplacement de cette clé, à partir du point de terminaison des métadonnées du serveur d'autorisation.

Il est important de noter que l'ajout de la propriété issuer-uri oblige à exécuter le serveur d'autorisation avant de pouvoir démarrer l'application Resource Server .

Voyons maintenant comment configurer la prise en charge JWT à l'aide de la configuration Java:

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**") .hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/api/foos") .hasAuthority("SCOPE_write") .anyRequest() .authenticated() .and() .oauth2ResourceServer() .jwt(); } }

Ici, nous remplaçons la configuration de sécurité Http par défaut. Nous devons donc spécifier explicitement que nous voulons que cela se comporte comme un serveur de ressources et que nous utiliserons des jetons d'accès au format JWT en utilisant respectivement les méthodes oauth2ResourceServer () et jwt () .

La configuration JWT ci-dessus est ce que l'instance Spring Boot par défaut nous fournit. Cela peut également être personnalisé comme nous le verrons sous peu.

4. Revendications personnalisées dans le jeton

Configurons maintenant une infrastructure pour pouvoir ajouter quelques revendications personnalisées dans le jeton d'accès renvoyé par le serveur d'autorisation . Les revendications standard fournies par le framework sont toutes bonnes, mais la plupart du temps, nous aurons besoin d'informations supplémentaires dans le jeton à utiliser du côté client.

Prenons un exemple de revendication personnalisée, organisation , qui contiendra le nom de l'organisation d'un utilisateur donné.

4.1. Configuration du serveur d'autorisation

Pour cela, nous devons ajouter quelques configurations à notre fichier de définition de domaine, baeldung-realm.json :

  • Ajouter une organisation attributaire à notre utilisateur [email protected] :
    "attributes" : { "organization" : "baeldung" },
  • Ajoutez une organisation appelée protocolMapper à la configuration jwtClient :
    "protocolMappers": [{ "id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1", "name": "organization", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "organization", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "organization", "jsonType.label": "String" } }],

Pour une configuration de Keycloak autonome, cela peut également être fait à l'aide de la console d'administration.

De plus, il est important de se rappeler que la configuration JSON ci-dessus est spécifique à Keycloak et peut différer pour d'autres serveurs OAuth .

Avec cette nouvelle configuration opérationnelle, nous aurons un attribut supplémentaire organization = baeldung , dans la charge utile du jeton pour [email protected] :

{ jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e" exp: 1585242462 nbf: 0 iat: 1585242162 iss: "//localhost:8083/auth/realms/baeldung" sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f" typ: "Bearer" azp: "jwtClient" auth_time: 1585242162 session_state: "384ca5cc-8342-429a-879c-c15329820006" acr: "1" scope: "profile write read" organization: "baeldung" preferred_username: "[email protected]" }

4.2. Utilisez le jeton d'accès dans le client angulaire

Ensuite, nous souhaitons utiliser les informations sur les jetons dans notre application Angular Client. Nous utiliserons la bibliothèque angular2-jwt pour cela.

Nous utiliserons la revendication d' organisation dans notre AppService et ajouterons une fonction getOrganization :

getOrganization(){ var token = Cookie.get("access_token"); var payload = this.jwtHelper.decodeToken(token); this.organization = payload.organization; return this.organization; }

Cette fonction utilise JwtHelperService de la bibliothèque angular2-jwt pour décoder le jeton d'accès et obtenir notre revendication personnalisée. Il ne nous reste plus qu'à l'afficher dans notre AppComponent :

@Component({ selector: 'app-root', template: ` Spring Security Oauth - Authorization Code 

{{organization}}

` }) export class AppComponent implements OnInit { public organization = ""; constructor(private service: AppService) { } ngOnInit() { this.organization = this.service.getOrganization(); } }

5. Accéder aux revendications supplémentaires dans le serveur de ressources

Mais, comment pouvons-nous accéder à ces informations du côté du serveur de ressources?

5.1. Accéder aux revendications du serveur d'authentification

C'est très simple: nous avons juste besoin d' extraire du org.springframework.security.oauth2.jwt.Jwt de AuthenticationPrincipal , comme nous le ferions pour tout autre attribut dans UserInfoController :

@GetMapping("/user/info") public Map getUserInfo(@AuthenticationPrincipal Jwt principal) { Map map = new Hashtable(); map.put("user_name", principal.getClaimAsString("preferred_username")); map.put("organization", principal.getClaimAsString("organization")); return Collections.unmodifiableMap(map); } 

5.2. Configuration pour ajouter / supprimer / renommer les revendications

Maintenant, que se passe-t-il si nous voulons ajouter plus de revendications du côté du serveur de ressources? Ou supprimer ou renommer certains?

Disons que nous voulons modifier la revendication d' organisation provenant du serveur d'authentification pour obtenir la valeur en majuscules. De plus, si la revendication n'est pas présente sur un utilisateur, nous devons définir sa valeur comme inconnue .

Pour ce faire, nous devrons d'abord ajouter une classe qui implémente l' interface Converter et utilise MappedJwtClaimSetConverter pour convertir les revendications :

public class OrganizationSubClaimAdapter implements Converter
    
      { private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); public Map convert(Map claims) { Map convertedClaims = this.delegate.convert(claims); String organization = convertedClaims.get("organization") != null ? (String) convertedClaims.get("organization") : "unknown"; convertedClaims.put("organization", organization.toUpperCase()); return convertedClaims; } }
    

Deuxièmement, dans notre classe SecurityConfig , nous devons ajouter notre propre instance JwtDecoder pour remplacer celle fournie par Spring Boot et définir notre OrganizationSubClaimAdapter comme son convertisseur de revendications :

@Bean public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri( properties.getJwt().getJwkSetUri()).build(); jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter()); return jwtDecoder; } 

Maintenant , quand nous avons atteint notre / utilisateur / informations API pour l'utilisateur [email protected] , nous obtenons l' organisation comme INCONNU .

Notez que remplacer le bean JwtDecoder par défaut configuré par Spring Boot doit être fait avec soin pour s'assurer que toute la configuration nécessaire est toujours incluse.

6. Chargement de clés à partir d'un keystore Java

Dans notre configuration précédente, nous avons utilisé la clé publique par défaut du serveur d'autorisation pour vérifier l'intégrité de notre jeton.

Nous pouvons également utiliser une paire de clés et un certificat stockés dans un fichier Java Keystore pour effectuer le processus de signature.

6.1. Générer un fichier JKS Java KeyStore

Générons d'abord les clés - et plus précisément un fichier .jks - à l'aide de l'outil de ligne de commande keytool :

keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass

La commande générera un fichier appelé mytest.jks qui contient nos clés - les clés publique et privée.

Assurez-vous également que le keypass et le storepass sont identiques.

6.2. Exporter la clé publique

Ensuite, nous devons exporter notre clé publique à partir de JKS généré, nous pouvons utiliser la commande suivante pour le faire:

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

Un exemple de réponse ressemblera à ceci:

-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2 /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3 DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK eQIDAQAB -----END PUBLIC KEY----- -----BEGIN CERTIFICATE----- MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1 czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2 MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3 1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0 yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp /J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF lLFCUGhA7hxn2xf3x1JW -----END CERTIFICATE-----

6.3. Configuration Maven

Ensuite, nous ne voulons pas que le fichier JKS soit récupéré par le processus de filtrage maven - nous nous assurerons donc de l'exclure dans le pom.xml :

   src/main/resources true  *.jks    

If we're using Spring Boot, we need to make sure that our JKS file is added to application classpath via the Spring Boot Maven Plugin – addResources:

   org.springframework.boot spring-boot-maven-plugin  true    

6.4. Authorization Server

Now, we will configure Keycloak to use our Keypair from mytest.jks, by adding it to the realm definition JSON file's KeyProvider section as follows:

{ "id": "59412b8d-aad8-4ab8-84ec-e546900fc124", "name": "java-keystore", "providerId": "java-keystore", "subComponents": {}, "config": { "keystorePassword": [ "mypass" ], "keyAlias": [ "mytest" ], "keyPassword": [ "mypass" ], "active": [ "true" ], "keystore": [ "src/main/resources/mytest.jks" ], "priority": [ "101" ], "enabled": [ "true" ], "algorithm": [ "RS256" ] } },

Here we have set the priority to 101, greater than any other Keypair for our Authorization Server, and set active to true. This is done to ensure that our Resource Server would pick this particular Keypair from the jwk-set-uri property we specified earlier.

Là encore, cette configuration est spécifique à Keycloak et peut différer pour d'autres implémentations de serveur OAuth.

7. Conclusion

Dans cet article rapide, nous nous sommes concentrés sur la configuration de notre projet Spring Security OAuth2 pour utiliser des jetons Web JSON.

L'implémentation complète de ce didacticiel se trouve dans over sur GitHub.