Usando JWT con Spring Security OAuth

1. Información general

En este tutorial, discutiremos cómo hacer que nuestra implementación Spring Security OAuth2 haga uso de JSON Web Tokens.

También continuamos construyendo sobre el artículo Spring REST API + OAuth2 + Angular en esta serie OAuth.

2. El servidor de autorización OAuth2

Anteriormente, la pila Spring Security OAuth ofrecía la posibilidad de configurar un servidor de autorización como una aplicación Spring. Luego tuvimos que configurarlo para usar JwtTokenStore para poder usar tokens JWT.

Sin embargo, Spring ha desaprobado la pila OAuth y ahora usaremos Keycloak como nuestro servidor de autorización.

Entonces, esta vez, configuraremos nuestro servidor de autorización como un servidor Keycloak integrado en una aplicación Spring Boot . Emite tokens JWT de forma predeterminada, por lo que no es necesario realizar ninguna otra configuración a este respecto.

3. Servidor de recursos

Ahora, echemos un vistazo a cómo configurar nuestro servidor de recursos para usar JWT.

Haremos esto en un archivo 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

Los JWT incluyen toda la información dentro del Token. Por lo tanto, el servidor de recursos debe verificar la firma del token para asegurarse de que los datos no se hayan modificado. El JWK-set-uri propiedad contiene la clave pública que el servidor puede usar para este propósito .

La propiedad issuer-uri apunta al URI del servidor de autorización base, que también se puede usar para verificar el reclamo de iss , como una medida de seguridad adicional.

Además, si no se establece la propiedad jwk-set-uri , el servidor de recursos intentará utilizar la interfaz de usuario del emisor para determinar la ubicación de esta clave, desde el punto final de metadatos del servidor de autorización.

Es importante destacar que, al agregar la propiedad issuer-uri , debemos tener el servidor de autorización ejecutándose antes de poder iniciar la aplicación del servidor de recursos .

Ahora veamos cómo podemos configurar el soporte JWT usando la configuración de 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(); } }

Aquí, anulamos la configuración de seguridad Http predeterminada. Por lo tanto, debemos especificar explícitamente que queremos que esto se comporte como un servidor de recursos y que usaremos tokens de acceso con formato JWT utilizando los métodos oauth2ResourceServer () y jwt () respectivamente.

La configuración JWT anterior es lo que nos proporciona la instancia predeterminada de Spring Boot. Esto también se puede personalizar como veremos en breve.

4. Reclamaciones personalizadas en el token

Configuremos ahora algo de infraestructura para poder agregar algunas reclamaciones personalizadas en el token de acceso devuelto por el servidor de autorización . Las afirmaciones estándar proporcionadas por el marco están muy bien, pero la mayoría de las veces necesitaremos información adicional en el token para utilizar en el lado del Cliente.

Tomemos un ejemplo de un reclamo personalizado, organización , que contendrá el nombre de la organización de un usuario determinado.

4.1. Configuración del servidor de autorización

Para esto, necesitamos agregar un par de configuraciones a nuestro archivo de definición de reino, baeldung-realm.json :

  • Agregue una organización de atributos a nuestro usuario [correo electrónico protegido] :
    "attributes" : { "organization" : "baeldung" },
  • Agregue un protocolMapper llamado organización a la configuración de 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" } }],

Para una configuración de Keycloak independiente, esto también se puede hacer usando la Consola de administración.

Además, es importante recordar que la configuración JSON anterior es específica de Keycloak y puede diferir de otros servidores OAuth .

Con esta nueva configuración en funcionamiento, obtendremos un atributo adicional organization = baeldung , en la carga útil del token para [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. Use el token de acceso en el cliente angular

A continuación, queremos hacer uso de la información del Token en nuestra aplicación Angular Client. Usaremos la biblioteca angular2-jwt para eso.

Usaremos el reclamo de organización en nuestro AppService y agregaremos una función getOrganization :

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

Esta función hace uso de JwtHelperService de la biblioteca angular2-jwt para decodificar el token de acceso y obtener nuestro reclamo personalizado. Ahora todo lo que tenemos que hacer es mostrarlo en nuestro 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. Acceda a reclamos adicionales en el servidor de recursos

Pero, ¿cómo podemos acceder a esa información desde el lado del servidor de recursos?

5.1. Acceder a las reclamaciones del servidor de autenticación

Eso es muy sencillo: sólo hay que extraerlo de la org.springframework.security.oauth2.jwt.Jwt 's AuthenticationPrincipal , como haríamos con cualquier otro atributo en 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. Configuración para agregar / quitar / cambiar el nombre de las reclamaciones

Ahora, ¿qué pasa si queremos agregar más reclamos en el lado del servidor de recursos? ¿O eliminar o cambiar el nombre de algunos?

Digamos que queremos modificar el reclamo de la organización que viene del servidor de autenticación para obtener el valor en mayúsculas. Además, si el reclamo no está presente en un usuario, debemos establecer su valor como desconocido .

Para lograr esto, primero, tendremos que agregar una clase que implemente la interfaz de Converter y use MappedJwtClaimSetConverter para convertir reclamos :

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

En segundo lugar, en nuestra clase SecurityConfig , debemos agregar nuestra propia instancia de JwtDecoder para anular la proporcionada por Spring Boot y configurar nuestro OrganizationSubClaimAdapter como su convertidor de reclamos :

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

Now when we hit our /user/info API for the user [email protected], we'll get the organization as UNKNOWN.

Note that overriding the default JwtDecoder bean configured by Spring Boot should be done carefully to ensure all the necessary configuration is still included.

6. Loading Keys From a Java Keystore

In our previous configuration, we used the Authorization Server's default public key to verify our token's integrity.

We can also use a keypair and certificate stored in a Java Keystore file to do the signing process.

6.1. Generate JKS Java KeyStore File

Let's first generate the keys – and more specifically a .jks file – using the command line tool keytool:

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

The command will generate a file called mytest.jks which contains our keys – the Public and Private keys.

Also make sure keypass and storepass are the same.

6.2. Export Public Key

Next, we need to export our Public key from generated JKS, we can use the following command to do so:

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

A sample response will look like this:

-----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. Maven Configuration

Next, we don't want the JKS file to be picked up by the maven filtering process – so we'll make sure to exclude it in the 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.

Nuevamente, esta configuración es específica de Keycloak y puede diferir para otras implementaciones de OAuth Server.

7. Conclusión

En este artículo rápido, nos enfocamos en configurar nuestro proyecto Spring Security OAuth2 para usar JSON Web Tokens.

La implementación completa de este tutorial se puede encontrar en GitHub.