Hace rato que OAUTH 2.0 viene estando presente en el escenario de seguridad en IT. El post que hoy nos ocupa tiene como propósito mostrar un ejemplo funcional del esquema Prueba de posesión de clave, del ingles Proof Of Possession (POP) of key usando Spring Boot.
Este ejemplo usa como base el código presentado en el maravilloso libro, OAUTH 2.0 Cookbook, la diferencia es que este SI FUNCIONA!!! ;)
Esta va a ser una publicación larga, digna del castigo que merecemos por el largo tiempo que llevamos de inactividad.
Como siempre decimos, manos a las sobras!!!, pero primero algunos conceptos básicos.
La Suite de Lujo es el recurso al que Ud. quiere acceder (Resource).
El Hotel es El Propietario del Recurso (Resource Server).
La Administración del hotel es quien Autoriza el Acceso (Authorization Server)
La Llave Digital es el Token de acceso al recurso (Access Token).
Y Ud., obviamente, es el Cliente o Tercero que quiere acceder al recurso (Client).
Asi que, para poder acceder al recurso, Ud., el cliente, primero debe estar registrado, ser autorizado y tener un Token de acceso válido. Y es así como OAUTH funciona.
Open Authorization (OAuth) es un estándar abierto que permite flujos simples de autorización para sitios web o aplicaciones informáticas. OAuth 2 es una estructura (framework) de autorización que le permite a las aplicaciones obtener acceso limitado a cuentas de usuario en un servicio HTTP, como Facebook, GitHub y DigitalOcean. Delega la autenticación del usuario al servicio que aloja la cuenta del mismo y autoriza a las aplicaciones de terceros el acceso a dicha cuenta de usuario. OAuth 2 proporciona flujos de autorización para aplicaciones web y de escritorio; y dispositivos móviles.
Para mayor información favor visitar el siguiente video: OAuth: When Things Go Wrong.
Ahora si... comencemos.
Para poder probar nuestro Provider, debemos ejecutar nuestra aplicación de Spring Boot y usar un cliente adecuado. Nuestra siguiente publicación cubrirá la creación de ese cliente de OAUTH 2.0.
Este ejemplo usa como base el código presentado en el maravilloso libro, OAUTH 2.0 Cookbook, la diferencia es que este SI FUNCIONA!!! ;)
Esta va a ser una publicación larga, digna del castigo que merecemos por el largo tiempo que llevamos de inactividad.
Como siempre decimos, manos a las sobras!!!, pero primero algunos conceptos básicos.
OAUTH 2.0
Imagine por un momento que Ud. llega a un hotel, se registra, y le dan una llave digital para entrar a su fabulosa suite. Luego, Ud. usa la llave que le facilitaron para poder tener acceso a la respectiva suite o habitación de lujo, en donde pasa una estupenda velada... ok, ok, ya vamos a hablar de OAUTH.La Suite de Lujo es el recurso al que Ud. quiere acceder (Resource).
El Hotel es El Propietario del Recurso (Resource Server).
La Administración del hotel es quien Autoriza el Acceso (Authorization Server)
La Llave Digital es el Token de acceso al recurso (Access Token).
Y Ud., obviamente, es el Cliente o Tercero que quiere acceder al recurso (Client).
Asi que, para poder acceder al recurso, Ud., el cliente, primero debe estar registrado, ser autorizado y tener un Token de acceso válido. Y es así como OAUTH funciona.
Open Authorization (OAuth) es un estándar abierto que permite flujos simples de autorización para sitios web o aplicaciones informáticas. OAuth 2 es una estructura (framework) de autorización que le permite a las aplicaciones obtener acceso limitado a cuentas de usuario en un servicio HTTP, como Facebook, GitHub y DigitalOcean. Delega la autenticación del usuario al servicio que aloja la cuenta del mismo y autoriza a las aplicaciones de terceros el acceso a dicha cuenta de usuario. OAuth 2 proporciona flujos de autorización para aplicaciones web y de escritorio; y dispositivos móviles.
Proof Of Possession
Es un mecanismo e implementación de autorización en OAUTH 2.0 en el que el cliente debe ser capaz de demostrar que posee una clave que valida el Token que dicho cliente pueda poseer. En otras palabras es un mecanismo para aumentar la seguridad de OAUTH 2.0, garantizando que un Token proviene desde una fuente confiable.Para mayor información favor visitar el siguiente video: OAuth: When Things Go Wrong.
Ahora si... comencemos.
OAuth Provider - Servidores
Vamos a comenzar por construir una aplicación que funcione como un Proveedor de OAUTH 2.0, la cual implementará en si misma los servidores de Autorización y de Recursos respectivamente. Esta será una aplicación de Spring Boot, Java 8, Maven, Spring Web, Spring Security, y Nimbus JOSE + JWT (el cual proveerá JWE y capacidades de encriptamiento). otras dependencias se verán en detalle al declararlas en el archivo .pom correspondiente.- Primero que todo, vamos a empezar por crear la aplicación pop-server usando el inicializador de spring: Spring Initializr.
Escoja un Tipo de proyecto Maven. Java como lenguaje. Sprig boot 2.1.3.
Ingrese su Group de preferencia como por ejemplo:
org.pigbar.hal9k.oauth2.pop
Ingrese el nombre del Artifact:
pop-server
En las dependencias agregue:
Web, Security y Thymeleaf
Genere el proyecto, descárguelo y proceda a importarlo en su IDE de preferencia como un proyecto Maven existente. - En su proyecto Maven verifique el contenido del archivo pom.xml, debe ser como el siguiente:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
<
project
xmlns:xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns
=
"http://maven.apache.org/POM/4.0.0"
xsi:schemalocation
=
"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<
modelversion
>4.0.0</
modelversion
>
<
parent
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-parent</
artifactid
>
<
version
>2.1.3.RELEASE</
version
>
<
relativepath
>
</
relativepath
></
parent
>
<
groupid
>org.pigbar.hal9k.oauth2.pop</
groupid
>
<
artifactid
>pop-server</
artifactid
>
<
version
>0.0.1-SNAPSHOT</
version
>
<
name
>pop-server</
name
>
<
description
>Demo project for Spring Boot</
description
>
<
properties
>
<
java
.version
=
""
>1.8</
java
>
</
properties
>
<
dependencies
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-security</
artifactid
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-thymeleaf</
artifactid
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-web</
artifactid
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-starter-test</
artifactid
>
<
scope
>test</
scope
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.security</
groupid
>
<
artifactid
>spring-security-test</
artifactid
>
<
scope
>test</
scope
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.security.oauth</
groupid
>
<
artifactid
>spring-security-oauth2</
artifactid
>
<
version
>2.2.0.RELEASE</
version
>
</
dependency
>
<
dependency
>
<
groupid
>org.springframework.security</
groupid
>
<
artifactid
>spring-security-jwt</
artifactid
>
<
version
>1.0.10.RELEASE</
version
>
</
dependency
>
<
dependency
>
<
groupid
>com.nimbusds</
groupid
>
<
artifactid
>nimbus-jose-jwt</
artifactid
>
<
version
>4.23</
version
>
</
dependency
>
</
dependencies
>
<
build
>
<
plugins
>
<
plugin
>
<
groupid
>org.springframework.boot</
groupid
>
<
artifactid
>spring-boot-maven-plugin</
artifactid
>
</
plugin
>
</
plugins
>
</
build
>
</
project
>
- Dentro de la carpeta src/maim/resources, en el archivo application.properties ingrese los siguientes registros de key = value:
1234spring.security.user.name=pigbar
spring.security.user.password=1234
spring.main.allow-bean-definition-overriding=
true
security.oauth2.resource.jwt.key-value=non-prod
- En el package org.pigbar.hal9k.oauth2.pop cree los siguientes sub-packages:
api
oauth.authorizationserver
oauth.resourceserver - Como el Authorization Server debe retornar un JWT Token cuando reciba una Token Request y la aplicación debe soportar la semantica de Proof Of Possession, sera necesario escribir una clase que implemente la interface TokenEnhancer. Esta clase sera la responsable de extraer la la clave pública, Public Key, enviada por el cliente en la fase de token request o solicitud de token. Esta clave pública sera usada como información adicional del JWT Token.
Vamos a crear la clase PoPTokenEnhancer como una implementación de la interface TokenEnhancer, dentro del package oauth.authorizationserver:
12345678910111213141516171819202122232425262728import
org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import
org.springframework.security.oauth2.common.OAuth2AccessToken;
import
org.springframework.security.oauth2.provider.OAuth2Authentication;
import
org.springframework.security.oauth2.provider.token.TokenEnhancer;
import
java.util.HashMap;
import
java.util.Map;
/**
* This class propagates the public_key sent by the PoP client request.
*/
class
PoPTokenEnhancer
implements
TokenEnhancer {
@Override
public
OAuth2AccessToken enhance(
OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String Object=
""
> additional =
new
HashMap<>();
String publicKey = authentication.getOAuth2Request().getRequestParameters().get(
"public_key"
);
additional.put(
"public_key"
, publicKey);
DefaultOAuth2AccessToken defaultAccessToken = (DefaultOAuth2AccessToken) accessToken;
defaultAccessToken.setAdditionalInformation(additional);
return
accessToken;
}
}
- Para evitar la duplicación de datos, creamos una clase que se encargue de remover el atributo sobrante public_key del tohen de respuesta en una solicitud de token. En el mismo paquete anterior creamos la siguiente clase:
1234567891011import
org.springframework.security.oauth2.common.OAuth2AccessToken;
import
org.springframework.security.oauth2.provider.OAuth2Authentication;
import
org.springframework.security.oauth2.provider.token.TokenEnhancer;
public
class
CleanTokenEnhancer
implements
TokenEnhancer {
@Override
public
OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
accessToken.getAdditionalInformation().remove(
"public_key"
);
return
accessToken;
}
}
- Es hora de crear la clase encargada de configurar el Authorization Server. En el mismo package creamos la siguiente clase:
1234567891011121314151617181920212223242526272829303132333435363738import
java.util.Arrays;
import
org.springframework.context.annotation.Bean;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import
org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import
org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import
org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import
org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import
org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
@Configuration
@EnableAuthorizationServer
public
class
OAuth2AuthorizationServer
extends
AuthorizationServerConfigurerAdapter {
@Bean
public
JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter =
new
JwtAccessTokenConverter();
converter.setSigningKey(
"non-prod"
);
return
converter;
}
@Override
public
void
configure(AuthorizationServerEndpointsConfigurer endpoints)
throws
Exception {
TokenEnhancerChain chain =
new
TokenEnhancerChain();
chain.setTokenEnhancers(
Arrays.asList(
new
PoPTokenEnhancer(), accessTokenConverter(),
new
CleanTokenEnhancer()));
endpoints.tokenEnhancer(chain);
}
@Override
public
void
configure(ClientDetailsServiceConfigurer clients)
throws
Exception {
clients.inMemory().withClient(
"clientapp"
).secret(
"{noop}123456"
).scopes(
"read_profile"
)
.authorizedGrantTypes(
"authorization_code"
);
}
}
Preste atención al detalle del código:
1clients.inMemory().withClient(
"clientapp"
).secret(
"{noop}123456"
).scopes(
"read_profile"
)
Donde el fragmento "{noop}" se agrega para especificar el password encoder, o su ausencia, para compatibilidad en spring security y evitar el conocido "Spring Boot PasswordEncoder Error" - Listo, ya tenemos un Authorization Server listo para proveer un JWT Token con la public_key requerida para que el cliente la pueda enviar al Resource Server y este último pueda validar la POP.
- Dicho esto es momento de empezar a codificar nuestro Resource Server. Primero vamos a crear una implementación de la interface Authentication, la cual sera, de hecho, un decorador de una implementación existente. Esta implementación tiene por objeto manejar el atributo nonce, el cual es usado como el valor generado que debe ser firmado por el servidor y el cliente. En el package oauth.resourceserver, vamos a crear la siguiente clase:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758import
org.springframework.security.core.Authentication;
import
org.springframework.security.core.GrantedAuthority;
import
java.util.Collection;
public
class
PoPAuthenticationToken
implements
Authentication {
private
Authentication authentication;
private
String nonce;
public
PoPAuthenticationToken(Authentication authentication) {
this
.authentication = authentication;
}
public
void
setNonce(String nonce) {
this
.nonce = nonce;
}
public
String getNonce() {
return
nonce;
}
@Override
public
Collection getAuthorities() {
return
authentication.getAuthorities();
}
@Override
public
Object getCredentials() {
return
authentication.getCredentials();
}
@Override
public
Object getDetails() {
return
authentication.getDetails();
}
@Override
public
Object getPrincipal() {
return
authentication.getPrincipal();
}
@Override
public
boolean
isAuthenticated() {
return
authentication.isAuthenticated();
}
@Override
public
void
setAuthenticated(
boolean
isAuthenticated)
throws
IllegalArgumentException {
authentication.setAuthenticated(isAuthenticated);
}
@Override
public
String getName() {
return
authentication.getName();
}
}
- Es momento de escribir la clase PoPTokenExtractor, la cual se va a encargar de crear una instancia de PoPAuthenticationToken y le establece en el Header el valor adecuado del atributo nonce.
1234567891011121314151617181920212223242526import
org.springframework.security.core.Authentication;
import
org.springframework.security.oauth2.provider.authentication.TokenExtractor;
import
javax.servlet.http.HttpServletRequest;
public
class
PoPTokenExtractor
implements
TokenExtractor {
private
TokenExtractor delegate;
public
PoPTokenExtractor(TokenExtractor delegate) {
this
.delegate = delegate;
}
@Override
public
Authentication extract(HttpServletRequest request) {
Authentication authentication = delegate.extract(request);
if
(authentication !=
null
) {
PoPAuthenticationToken popAuthenticationToken =
new
PoPAuthenticationToken(authentication);
popAuthenticationToken.setNonce(request.getHeader(
"nonce"
));
return
popAuthenticationToken;
}
return
authentication;
}
}
- Como estamos usando atributos particulares propios, es necesario entonces que el proceso de Autorización sea personalizado. Es por ello que en el mismo package anterior vamos a crear la clase siguiente:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364import
com.nimbusds.jose.JWSObject;
import
com.nimbusds.jose.JWSVerifier;
import
com.nimbusds.jose.crypto.RSASSAVerifier;
import
com.nimbusds.jose.jwk.JWK;
import
com.nimbusds.jose.jwk.RSAKey;
import
com.nimbusds.jwt.JWT;
import
com.nimbusds.jwt.JWTParser;
import
org.springframework.security.authentication.AuthenticationManager;
import
org.springframework.security.core.Authentication;
import
org.springframework.security.core.AuthenticationException;
import
org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import
org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
public
class
PoPAuthenticationManager
implements
AuthenticationManager {
private
AuthenticationManager authenticationManager;
public
PoPAuthenticationManager(AuthenticationManager authenticationManager) {
this
.authenticationManager = authenticationManager;
}
@Override
public
Authentication authenticate(Authentication authentication)
throws
AuthenticationException {
Authentication authenticationResult = authenticationManager
.authenticate(authentication);
if
(authenticationResult.isAuthenticated()) {
// validates nonce because JWT is already valid
if
(authentication
instanceof
PoPAuthenticationToken) {
PoPAuthenticationToken popAuthentication = (PoPAuthenticationToken) authentication;
// starts validating nonce here
String nonce = popAuthentication.getNonce();
if
(nonce ==
null
) {
throw
new
UnapprovedClientAuthenticationException(
"This request does not have a valid signed nonce"
);
}
String token = (String) popAuthentication.getPrincipal();
System.out.println(
"access token:"
+ token);
try
{
JWT jwt = JWTParser.parse(token);
String publicKey = jwt.getJWTClaimsSet().getClaim(
"public_key"
).toString();
JWK jwk = JWK.parse(publicKey);
JWSObject jwsNonce = JWSObject.parse(nonce);
JWSVerifier verifier =
new
RSASSAVerifier((RSAKey) jwk);
if
(!jwsNonce.verify(verifier)) {
throw
new
InvalidTokenException(
"Client hasn't possession of given token"
);
}
}
catch
(Exception e) {
throw
new
RuntimeException(e);
}
}
}
return
authenticationResult;
}
}
- Ahora si, vamos a crear la clase OAuth2ResourceServer, la cual estará a cargo de la configuración del Resource Server:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061import
org.springframework.context.annotation.Bean;
import
org.springframework.context.annotation.Configuration;
import
org.springframework.context.annotation.Primary;
import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
import
org.springframework.security.jwt.crypto.sign.MacSigner;
import
org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import
org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import
org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import
org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import
org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import
org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager;
import
org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import
org.springframework.security.oauth2.provider.token.TokenStore;
import
org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import
org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableResourceServer
public
class
OAuth2ResourceServer
extends
ResourceServerConfigurerAdapter {
@Bean
@Primary
public
DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices =
new
DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
return
tokenServices;
}
@Bean
public
TokenStore tokenStore() {
JwtTokenStore tokenStore =
new
JwtTokenStore(accessTokenConverter());
return
tokenStore;
}
@Bean
public
JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter =
new
JwtAccessTokenConverter();
converter.setVerifier(verifier());
converter.setSigningKey(
"non-prod"
);
return
converter;
}
@Bean
public
SignatureVerifier verifier() {
return
new
MacSigner(
"non-prod"
);
}
@Override
public
void
configure(ResourceServerSecurityConfigurer resources)
throws
Exception {
resources.tokenExtractor(
new
PoPTokenExtractor(
new
BearerTokenExtractor()));
OAuth2AuthenticationManager oauth =
new
OAuth2AuthenticationManager();
oauth.setTokenServices(tokenServices());
resources.authenticationManager(
new
PoPAuthenticationManager(oauth));
}
@Override
public
void
configure(HttpSecurity http)
throws
Exception {
http.authorizeRequests().anyRequest().authenticated().and().requestMatchers().antMatchers(
"/api/**"
);
}
}
- Como nuestro OAuth Provider es también nuestro Authorization Server y nuestro Resource Server, ahora crearemos una API REST para que sea consumida desde un Cliente OAUTH 2.0. Así que vayamos al package api y creemos la siguiente clase:
12345678910111213141516171819202122232425262728293031323334353637383940import
org.springframework.http.ResponseEntity;
import
org.springframework.security.core.context.SecurityContextHolder;
import
org.springframework.stereotype.Controller;
import
org.springframework.web.bind.annotation.RequestMapping;
@Controller
public
class
UserController {
@RequestMapping
(
"/api/profile"
)
public
ResponseEntity<UserProfile< myProfile() {
String username = (String) SecurgtyContextHolder.getContext()
.getAuthentication().getPrincipal();
String email = username +
"@mailinator.com"
;
UserProfile profile =
new
UserProfile(username, email);
return
ResponseEntity.ok(profile);
}
public
static
class
UserProfile {
private
String name;
private
String email;
public
UserProfile(String name, String email) {
this
.name = name;
this
.email = email;
}
public
String getName() {
return
name;
}
public
String getEmail() {
return
email;
}
}
}
- Para finalizar, debemos crear una clase encargada de establecer la configuración de acceso al servidor y sus recursos. En el mismo package, api, creemos la proxima clase:
1234567891011121314import
org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.web.builders.HttpSecurity;
import
org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import
org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public
class
WebSecurityConfig
extends
WebSecurityConfigurerAdapter {
@Override
protected
void
configure(HttpSecurity http)
throws
Exception {
http.formLogin().permitAll().and().authorizeRequests().antMatchers(
"/"
,
"/oauth/**"
).authenticated()
.anyRequest().authenticated();
}
}
Para poder probar nuestro Provider, debemos ejecutar nuestra aplicación de Spring Boot y usar un cliente adecuado. Nuestra siguiente publicación cubrirá la creación de ese cliente de OAUTH 2.0.
Comentarios
Publicar un comentario