Ir al contenido

Spring SecurityLección 10

OAuth2 Login, clientes y Authorization Server

Cuando hay varias aplicaciones, la emisión de tokens se centraliza. Login con un proveedor externo, el patrón BFF y tu propio Authorization Server, que en la 7 ya forma parte de Spring Security.

Avanzado20 min de lectura

Al terminar sabrás

  • Mapear los roles de OAuth2 a las piezas de Spring Security 7.
  • Iniciar sesión con un proveedor OIDC usando oauth2Login.
  • Montar un Authorization Server con oauth2AuthorizationServer y clientes por propiedades.
  • Conectar una API (resource server) a ese emisor.

En la lección de JWT la propia API emitía sus tokens. Cuando hay varias aplicaciones, varios clientes o login con Google, ese trabajo se centraliza en un Authorization Server: el único que ve contraseñas y el único que firma tokens. Los conceptos están en OAuth 2 y OpenID Connect; aquí vemos las piezas de Spring.

Rol OAuth2Qué hacePieza en Spring Security 7
Resource ownerEl usuario dueño de los datos—
ClientLa app que pide tokensoauth2Login() (login de usuarios) y oauth2Client() (llamar APIs con token)
Authorization ServerAutentica y emite tokensoauth2AuthorizationServer(): antes un proyecto aparte, ahora dentro de Spring Security
Resource ServerLa API que valida tokensoauth2ResourceServer()
  1. Navegadorsolo una cookie HttpOnly
  2. BFF / weboauth2Login + oauth2Client
  3. Authorization Serveroauth2AuthorizationServer
  4. APIoauth2ResourceServer
El patrón BFF: los tokens viven en el servidor; el navegador nunca los ve.

Con el starter spring-boot-starter-security-oauth2-client, iniciar sesión con Google (o Keycloak, Okta, Entra ID…) son propiedades:

application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
SecurityConfig.java
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/error").permitAll()
.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults()) // /oauth2/authorization/google inicia el flujo
.logout(Customizer.withDefaults());
return http.build();
}

El principal resultante es un OidcUser con los claims del id_token. Spring hace por ti el flujo Authorization Code con PKCE (activado por defecto en la 7, también para clientes confidenciales), valida state y nonce, canjea el code y verifica el id_token.

Este es el patrón BFF (Backend for Frontend): el backend guarda los tokens en la sesión del servidor y el navegador solo tiene una cookie HttpOnly. Es hoy la recomendación para SPAs, porque ningún token queda al alcance de JavaScript. Si el BFF necesita llamar a tu API con el token del usuario, oauth2Client gestiona el access token (y su renovación con el refresh token).

El servidor: Authorization Server dentro de Spring Security

Sección titulada «El servidor: Authorization Server dentro de Spring Security»

Desde la 7.0 el Authorization Server es un módulo de Spring Security: su versión es la de spring-security.version. Starter: spring-boot-starter-security-oauth2-authorization-server. La configuración mínima tiene dos cadenas: una para los endpoints del protocolo y otra para el login de los usuarios.

AuthServerConfig.java
@Bean
@Order(1)
SecurityFilterChain authorizationServer(HttpSecurity http) throws Exception {
http
.oauth2AuthorizationServer(as -> {
http.securityMatcher(as.getEndpointsMatcher()); // /oauth2/authorize, /oauth2/token, /oauth2/jwks…
as.oidc(Customizer.withDefaults()); // activa OpenID Connect
})
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.exceptionHandling(ex -> ex.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"), // navegadores → formulario
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain login(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}

Además necesita un RegisteredClientRepository (qué clientes existen), un JWKSource (con qué claves firma) y un UserDetailsService. Spring Boot puede crear los dos primeros a partir de propiedades:

application.yml (auth-server)
server:
port: 9000
spring:
security:
user:
name: ana
password: "{noop}ana-password-123" # solo para el lab
oauth2:
authorizationserver:
client:
notes-cli:
registration:
client-id: notes-cli
client-secret: "{noop}cli-secret"
client-authentication-methods: client_secret_basic
authorization-grant-types: client_credentials
scopes: notes:read
postman:
registration:
client-id: postman
client-secret: "{noop}postman-secret"
client-authentication-methods: client_secret_basic
authorization-grant-types: authorization_code, refresh_token
redirect-uris: https://oauth.pstmn.io/v1/callback
scopes: openid, profile, notes:read, notes:write
require-authorization-consent: true

Endpoints que expone, todos descubribles en /.well-known/openid-configuration:

EndpointPara qué
/oauth2/authorizeInicio del flujo con el usuario
/oauth2/tokenCanjear code, client credentials o refresh token
/oauth2/jwksClaves públicas para verificar firmas
/oauth2/introspect · /oauth2/revokeConsultar o revocar un token
/userinfoDatos del usuario (OIDC)

Y la API pasa a confiar en él con una sola propiedad:

application.yml (API)
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000

Mini lab · 45 min

Tu propio Authorization Server

Objetivo: Sacar la emisión de tokens de la API de notas y delegarla en un Authorization Server en el puerto 9000.

  1. Crea un proyecto nuevo auth-server con spring-boot-starter-webmvc y spring-boot-starter-security-oauth2-authorization-server. Copia las dos cadenas y el application.yml de arriba.
  2. En la API de notas, borra el endpoint de token, el JwtEncoder, el JwtDecoder y las claves RSA, y configura issuer-uri: http://localhost:9000.
  3. Obtén un token de servicio y úsalo:
Ventana de terminal
TOKEN=$(curl -s -u notes-cli:cli-secret \
-d grant_type=client_credentials -d scope=notes:read \
http://localhost:9000/oauth2/token | jq -r .access_token)
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/notes
  1. En Postman, pestaña Authorization → OAuth 2.0 → Authorization Code (With PKCE) con los datos del cliente postman. Inicia sesión como ana, acepta el consentimiento y llama a GET /api/notes.

Preguntas:

  • Abre http://localhost:9000/.well-known/openid-configuration. ¿Qué URL usa la API para descargar las claves?
  • Compara el sub de un token client_credentials con el de un token de ana. Si el primero creara una nota, ¿de quién sería?
  • Pide scope=notes:write con notes-cli. ¿Qué responde el Authorization Server?
  • Reinicia el Authorization Server y reutiliza un token viejo en la API. ¿Qué pasa?
Pista

En client_credentials no hay usuario: ¿quién es el “sujeto” del token?

Mostrar respuestaOcultar respuesta
  • La del campo jwks_uri: http://localhost:9000/oauth2/jwks. La API la descubre a partir de issuer-uri y cachea las claves.
  • En client_credentials el sub es el client_id (notes-cli), no un usuario. Si ese token pudiera crear notas, quedarían a nombre de notes-cli. Por eso a los clientes de servicio se les dan scopes mínimos (aquí, solo lectura) y la API debe distinguir tokens de usuario y de servicio cuando importa.
  • 400 con {"error":"invalid_scope"}: el cliente no tiene registrado ese scope. El Authorization Server aplica el mínimo privilegio por cliente.
  • La API responde 401 con invalid_token: el token viejo está firmado con una clave que ya no existe (se generó en memoria y se perdió al reiniciar). En producción, las claves deben ser persistentes y rotarse publicando la nueva antes de retirar la vieja.