Ir al contenido principal

Usando proof-of-possession key semantics con un Provider de OAuth 2.0

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.

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.


  1. 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.
  2. En su proyecto Maven verifique el contenido del archivo pom.xml, debe ser como el siguiente:
    
    
     4.0.0
     
      org.springframework.boot
      spring-boot-starter-parent
      2.1.3.RELEASE
       
     
     org.pigbar.hal9k.oauth2.pop
     pop-server
     0.0.1-SNAPSHOT
     pop-server
     Demo project for Spring Boot
    
     
      1.8
     
    
     
      
       org.springframework.boot
       spring-boot-starter-security
      
      
       org.springframework.boot
       spring-boot-starter-thymeleaf
      
      
       org.springframework.boot
       spring-boot-starter-web
      
    
      
       org.springframework.boot
       spring-boot-starter-test
       test
      
      
       org.springframework.security
       spring-security-test
       test
      
      
       org.springframework.security.oauth
       spring-security-oauth2
       2.2.0.RELEASE
      
      
       org.springframework.security
       spring-security-jwt
       1.0.10.RELEASE
      
      
       com.nimbusds
       nimbus-jose-jwt
       4.23
      
     
    
     
      
       
        org.springframework.boot
        spring-boot-maven-plugin
       
      
     
    
  3. Dentro de la carpeta src/maim/resources, en el archivo application.properties ingrese los siguientes registros de key = value:

    spring.security.user.name=pigbar
    spring.security.user.password=1234
    spring.main.allow-bean-definition-overriding=true
    security.oauth2.resource.jwt.key-value=non-prod 
    

  4. En el package org.pigbar.hal9k.oauth2.pop cree los siguientes sub-packages:

    api
    oauth.authorizationserver
    oauth.resourceserver
  5. 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:

    import 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;
        }
    }
    
  6.  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:

    import 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;
        }
    }
    
  7.  Es hora de crear la clase encargada de configurar el Authorization Server. En el mismo package creamos la siguiente clase:

    import 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:

    clients.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"
  8. 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.
  9. 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:

    import 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();
        }
    }
    
  10.  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.

    import 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;
        }
    }
    

     
  11. 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:

    import 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;
        }
    
    }
    
  12. Ahora si, vamos a crear la clase OAuth2ResourceServer, la cual estará a cargo de la configuración del Resource Server:

    import 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/**");
     }
    
    }
    
  13. 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:

    import 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;
            }
    
        }
    }
    
  14. 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:

    import 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

Entradas populares de este blog

El Melange todavía corre

Ese era el estribillo de un capítulo de unas de mis series favoritas de la infancia, Meteoro o Speed Racer. En ese capítulo un auto “fantasma” el X-3, aparecía de imprevisto y dejaba a todos asombrados con su rendimiento y prestaciones y volvía a desaparecer. Traigo ese episodio a colación puesto que recientemente sostuve una amena charla con un querido amigo, en la que el me manifestaba como los Mainframes habían muerto, o mejor dicho, el concepto de la computación distribuida basada en Mainframes había desaparecido. Para variar, yo no estuve de acuerdo, y le dije que por el contrario, el modelo de computación basado en Mainframes está mas vigente que nunca. Estos fueron mis argumentos:

Primeros pasos con Camunda BPM – Modelando un Proceso BPMN 2.0

Tenemos entre manos la tercera publicación de nuestra serie sobre la Plataforma de BPM de Camunda .  El día de hoy vamos, por fin, a empezar a modelar o construir nuestro primer proceso sencillo en notación BPMN 2.0. Para ello vamos a usar el modelador o editor que ya hemos instalado en nuestra primera publicación , y vamos a guardarlo en la sección de recursos del proyecto Maven Java que configuramos en la segunda publicación . Así que, como ya es costumbre, manos a las sobras…

Como configurar jBPM para usar nuestra propia Base de Datos en un sólo paso

Llevo un buen rato trabajando con jBPM en su serie 6.x, y mi opinión sobre este producto en la versión mecionada no ha mejorado para nada. Es una herramienta plena de funciones y caracteristicas avanzadas, pero tambien está llena de Bugs y es realmente inestable, sobre todo en el ambiente de modelamiento.  Así mismo, debo decir que tiene una muy aceptable API REST y que el motor de procesos y la consecuente ejecución de los procesos es estable y bastante rápida. En esta publicación daré inicio a una serie de artículos que hablan sobre ciertas configuraciones comunes e importantes que se hacen con jBPM. Hoy iniciamos con la configuración de jBPM para que use nuestra base de datos favorita. Esto tiene sentido porque el producto viene con la base de datos H2 por omisión, la cual es excelente para pruebas y evaluaciones rápidas de la herramienta, pero es completamente inaceptable en un ambiente de desarrollo, QA o producción cualquiera. Así que manos a l