Autorización con OpenID Connect y Spring Security

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).
Tarjetas de identificación en una mesa, simbolizando la autenticación y autorización de usuarios en un sistema

Í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.

Java
@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.

Java
@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.

Java
@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.

YAML
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.

Java
@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

Comentarios

2 respuestas

  1. Artículo interesante cuanto menos.

    Teniendo en cuenta el artículo de tu compañero y éste escrito por ti, cual sería la mejor forma de llevar a cabo un ejemplo real de todo esto?

    1. Buenas,

      Tanto el código adjunto como el proyecto de Spring son totalmente operativos para casos reales. Hay que adaptarlo a tu entorno y tus necesidades. Los roles, jerarquías, permisos o los detalles de los usuarios, son propios de cada organización. La potencia de esta herramienta reside en que no está limitada a lo que te ofrecen un productos concretos, se puede personalizar lo que se quiera.
      Te recomiendo que analices las necesidades en cuanto a gestión de identidades, autorizaciones de tu proyecto, los flujos más importantes a cubrir y valores entre las diferentes opciones en el mercado.

      Espero que te sirva.

      Un saludo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

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

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

  • Responsable: IZERTIS S.A.
  • Finalidad: Envío información de carácter administrativa, técnica, organizativa y/o comercial sobre los productos y servicios sobre los que se nos consulta.
  • Legitimación: Consentimiento del interesado
  • Destinatarios: Otras empresas del Grupo IZERTIS. Encargados del tratamiento.
  • Derechos: Acceso, rectificación, supresión, cancelación, limitación y portabilidad de los datos.
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad

Consultor tecnológico de desarrollo de proyectos informáticos. Puedes encontrarme en Autentia by Izertis: Ofrecemos servicios de soporte a desarrollo, factoría y formación. Somos expertos en Java/Java EE.

¿Quieres publicar en Adictos al trabajo?

Te puede interesar

Aprende cómo migrar tu sitio Joomla 3 a Joomla 5 de forma segura, manteniendo el diseño, la funcionalidad y compatibilidad con extensiones. Una guía paso a paso con recomendaciones, imágenes y buenas prácticas para actualizar sin sorpresas.
Descubre qué es Yocto Project, sus ventajas, usos reales en Izertis y cómo crear tu propia distribución Linux para Raspberry Pi paso a paso, de forma sencilla y flexible.
¿Trabajas con Drupal y SonarQube 9.9? En este artículo exploramos cómo adaptar el análisis estático para evitar falsos positivos, desactivar reglas conflictivas del Quality Profile y delegar el estilo a PHP CodeSniffer. Una guía práctica para mejorar la integración sin depender aún de SonarQube 10.