Autorización con OpenID Connect y Spring Security

0
166
Tarjetas de identificación en una mesa, simbolizando la autenticación y autorización de usuarios en un sistema
Autenticación segura de usuarios con OAuth2 y PKCE

En este tutorial recorremos las posibilidades que nos ofrece OAuth2 para proteger la identidad y las autorizaciones de tus usuarios, reforzado con la extensión Proof Key for Code Exchange (PKCE).

Índice

  1. Introducción
  2. Entorno de desarrollo
  3. Conceptos y estándares
  4. Ejemplo
  5. Pruebas
  6. Conclusiones
  7. Referencias

 

1. Introducción

Primeramente, revisaremos los principales estándares implicados en el proceso de autenticación y autorización con OAuth2 antes de mostrar un ejemplo de componentes que permiten visualizar este «baile» de mensajería. Este tutorial profundiza en el tutorial previo de mi compañero Ignacio Rubio Vázquez. Antes de entrar en lo más interesante, repasemos algunos conceptos para entender mejor las posibilidades de esta tecnología.

 

2. Entorno de desarrollo

  • Hardware: Portátil MacBook Pro 15′ (32GB DDR4).
  • Sistema Operativo: Mac OS Ventura 13.7.1
  • Spring Security: 6.2.1
  • Spring Authorization Server: 1.2.1
  • Spring Resource Server: 6.2.1

3. Conceptos y estándares

A continuación veamos el flujo Authorization Code Flow en OAuth2 que utilizaremos en este tutorial y las piezas más importantes:

Diagrama del flujo Authorization Code en OAuth 2.0 con extensión PKCE para proteger la autenticación y autorización
Este diagrama muestra el flujo de Authorization Code en OAuth 2.0 reforzado con PKCE, destacando los pasos de autenticación y la obtención de tokens para acceder a los recursos de una API.

Oauth2: Authorization code

OAuth 2.0 está diseñado para la autorización, para conceder acceso a los recursos de una aplicación a otra. El establecimiento de una sesión suele denominarse autenticación, y la información sobre la persona (el propietario del recurso) se denomina identidad. Cuando un servidor de autorización es compatible con OIDC, a veces se le denomina proveedor de identidad, ya que proporciona información sobre el propietario del recurso al cliente.
El Authorization Code Grant Type es un flujo estandarizado de OAuth2 que se utiliza en clientes de confianza y públicos para intercambiar un código de autorización hasta obtener un token de acceso (access token).

OpenID Connect

OpenID Connect (OIDC) es una capa adicional que se apoya sobre OAuth 2.0 y que añade información sobre el inicio de sesión y el perfil de la persona que inicia la sesión.
OIDC permite escenarios en los que se puede utilizar un inicio de sesión en varias aplicaciones, lo que también se conoce como inicio de sesión único (SSO). OAuth2 proporciona diferentes flujos dependiendo del caso de uso, hay varios, hoy nos centraremos en el más extendido, recomendado y utilizado por ser el más seguro para aplicaciones de tipo SPAs.

PKCE

Proof Key for Code Exchange (PKCE) RFC 7636, se pronuncia «pixie», es una extensión del flujo de código de autorización para prevenir ataques CSRF y de inyección de código de autorización.

JWT

JSON web token (JWT), pronunciado «jot», es un estándar abierto RFC 7519 que define una forma compacta y autocontenida de transmitir información de forma segura entre las partes como un objeto JSON. De nuevo, JWT es un estándar, lo que significa que todos los JWT son tokens, pero no todos los tokens son JWT. Son una forma sencilla, escalable para contener información sobre la autenticación y autorización del usuario. Debe tener la información justa y necesaria evitando que sea muy grandes o tengan más información de la necesaria por si hay filtraciones, se recomienda tener un tiempo de expiración corto.

AuthorizationServer o IDP

Un proveedor de identidades (IdP) es un sistema que autentica las identidades de los usuarios y autoriza su acceso a diversas aplicaciones y servicios mediante la gestión y verificación de credenciales digitales. Hay múltiples en el mercado, para este ejemplo, utilizaremos Spring Authorization Server por su simplicidad y sencillez en la integración con Spring Security, tenéis más detalles en la documentación de Spring.

Resource Server

En el contexto de OAuth 2.0, un servidor de recursos es una aplicación que protege los recursos mediante tokens OAuth. Estos tokens son emitidos por un servidor de autorización, normalmente a una aplicación cliente.

4. Ejemplo

¡Por fin! Al principio, todos estos detalles marean y son difíciles de seguir. Os pongo el código de esta demo en el repositorio de Github.
Tomaremos con referencia los proyectos de ejemplo que publica Spring Authorization Server en Github para configurar un flujo OAuth2 similar al expuesto anteriormente.
Incorporo un pequeño diagrama, sin entrar en detalle sobre los parámetros para que os hagáis un idea. Los artefactos de código mapean con el diagrama previo de la siguiente forma:

Diagrama que muestra el flujo de autorización de OAuth 2.0 con PKCE, destacando los pasos de autenticación y la obtención de tokens.
Flujo de Authorization Code en OAuth 2.0 con PKCE, destacando la autenticación, obtención de código de autorización, y el intercambio por tokens de acceso e identidad.

Si vemos la configuración de cada componente, tenemos:

Resource Server API (messages-resources).

Es el servicio que contiene el API con la funcionalidad deseada y que está protegida. Si veis la clase ResourceServerConfig, se observa como esos endpoint están protegido por el scope «SCOPE_message.read» o un rol que incorpora nuestro token.

@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class ResourceServerConfig {

    // @formatter:off
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/messages/**")
                .authorizeHttpRequests(authorize ->
                        authorize
                                .requestMatchers("/messages/**").hasAnyAuthority("SCOPE_message.read", "ROLE_user")
                )
                .oauth2ResourceServer(resourceServer ->
                        resourceServer
                                .jwt(jwt ->
                                        jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
                );
        return http.build();
    }
    // @formatter:on
    // 

En las dependencias podéis ver que utiliza incorpora la librería resource server para realizar la validación del token. Observad que el código es agnóstico de la solución que se aplica para validar el token y que no se contamina con detalles ajenos a la lógica propia de negocio.

@RestController
public class MessagesController {

    @GetMapping("/messages")
    public String[] getMessages() {
        return new String[] {"Message 1", "Message 2", "Message 3"};
    }
}

AuthorizationServer (default-authorizationServer)

En este caso, hemos configurado el servidor para que arranque con la configuración por defecto. Se le indica que utilice un inicio de sesión estándar y se han generado los componentes de Spring necesarios para realizar la autenticación. Recordad que los mecanismos de autenticación están fuera del alcance de OAuth y que se configuran como en cualquier aplicación con Spring Security.


@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        // @formatter:off
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());   // Enable OpenID Connect 1.0
        // @formatter:on

        // Configuración de redirección al inicio de sesión
        http
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                .oauth2ResourceServer(resourceServer -> 
                        resourceServer.jwt(Customizer.withDefaults()));
        return http.build();
    }
}

A nivel de OAuth2, el registro de la aplicación cliente (SPA) es interesante, ya que permite configurar los parámetros del flujo OAuth2 que seguirá nuestro cliente.


spring:
  security:
    user:
      name: user1
      password: password
      roles: admin, user
    oauth2:
      authorizationserver:
        client:
          messaging-client:
            registration:
              client-id: "messaging-client"
              client-secret: "{noop}secret"
              client-authentication-methods:
                - "client_secret_basic"
              authorization-grant-types:
                - "authorization_code"
                - "refresh_token"
                - "client_credentials"
              redirect-uris:
                - "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc"
              post-logout-redirect-uris:
                - "http://127.0.0.1:8080/logged-out"
              scopes:
                - "openid"
                - "profile"
                - "message.read"
                - "message.write"
            require-authorization-consent: false
            require-proof-key: true

SPA (demo-client)

Las funciones de la SPA se centran en establecer el diálogo con el IDP para negociar la obtención de los tokens. Posteriormente, se encarga de realizar llamadas a los recursos necesarios enviando el token de acceso mediante la clase WebClient.


@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);

        return WebClient.builder()
                .apply(oauth2Client.oauth2Configuration())
                .build();
    }

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {

        OAuth2AuthorizedClientProvider authorizedClientProvider =
                OAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .refreshToken()
                        .clientCredentials()
                        .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
}

 

5. Pruebas

¡Vamos al lío! Ha llegado el momento de probarlo. En el repositorio de código tenéis disponible Gradle para generar y ejecutar el proyecto. También podéis utilizar vuestro IDE favorito. Al tratarse de proyectos Spring Boot, la ejecución es inmediata. Si intentáis acceder a la web, se muestra la pantalla de inicio de sesión, cuyo usuario es user1/password.

Captura de pantalla del proceso de inicio de sesión, con una solicitud de autorización OAuth2 tipo Authorization Code con PKCE.
Pantalla de inicio de sesión mostrando la petición OAuth2 de tipo Authorization Code con PKCE y la redirección al login.

Una vez logados, en el menú superior podéis simular dos flujos de OAuth2: authorization code y client credentials. Al invocarlos, podéis recuperar los tokens y visualizarlos con jwt.io.

Captura de pantalla mostrando los valores de los tokens ID y Access Token generados en el flujo OAuth2
Muestra de los valores de los tokens ID y Access Token obtenidos tras completar el flujo OAuth2

Os recomiendo abrir la ventana de inspección de vuestro navegador y las trazas de logs para observar el flujo que sigue y los parámetros de PKCE.

Captura de pantalla mostrando la visualización de los tokens ID y Access Token en JWT.io, con los datos codificados y los valores de "scopes" y "roles"
Visualización detallada de los tokens ID y Access Token utilizando JWT.io, destacando los «scopes» y «roles» asociados a los tokens.

Antes de las conclusiones, es importante indicar que este ejemplo no incorpora suficientes protecciones para desplegar soluciones reales. Por ejemplo, el token de acceso no se devuelve tal cual, ya que podría exponer demasiada información del usuario; en su lugar, se suele utilizar un token "opaco".

 

6. Conclusiones

Este ejemplo muestra las partes importantes de un flujo OAuth2 utilizando un cliente público o potencialmente inseguro. Todo lo expuesto proporciona un mecanismo de tokens que reduce las posibilidades de intercepción, siendo este el flujo más utilizado hoy en día. Mediante OpenID podemos recuperar información adicional del usuario para mejorar su experiencia, y el token de acceso y el flujo implementado proporcionan un nivel mínimo de seguridad conforme a los estándares actuales. En la documentación se pueden encontrar mejoras adicionales.

El mundo de la autenticación y autorización está en constante evolución, buscando resolver los numerosos desafíos derivados del incremento continuo de ataques que intentan extraer el activo más valioso: tu información. En este tutorial se presenta un enfoque actual a estos problemas, basado en estándares, bibliotecas y buenas prácticas recomendadas a día de hoy. En el futuro, surgirán nuevos enfoques y mejoras.

Mi consejo: seguid los estándares actuales, mantened las versiones actualizadas y evitad trucos poco confiables. Espero que este tutorial os sea de ayuda.

 

7. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad