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
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:
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:
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.
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.
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
.
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
- Morgang, A.J. (n.d.). OpenID Connect Demo. GitHub. Retrieved from
https://github.com/ajmorgang/openid-connect-demo
- Connect2id. (n.d.). OAuth 2.1 – The Next Evolution of the OAuth 2.0 Framework. Connect2id. Retrieved from
https://connect2id.com/learn/oauth-2-1
- OpenID Foundation. (n.d.). OpenID Connect. Retrieved from
https://openidconnect.net/
- Pivotal Software, Inc. (n.d.). Getting Started with Spring Authorization Server. Spring. Retrieved from
https://docs.spring.io/spring-authorization-server/reference/getting-started.html