Ir al contenido principal

Usando proof-of-possession of key del lado del cliente


En nuestro capítulo anterior  vimos cómo implementar Proof Of Key en el lado del servidor usando OAUTH 2.0 con SpringBoot. Hoy corresponde enfocarnos en ese concepto del lado del cliente. En esta oportunidad vamos a desarrollar una aplicación que interactúe con el servidor o proveedor de OAUTH tras demostrar que está en posesión de la clave privada relacionada con la clave pública que se nos provee en el JWT correspondiente.
Al igual que la anterior, esta publicación va a hacer uso de Java 8, Maven, Spring Web, Spring Security y Nimbus JOSE + JWT.



Así que sin mucho preámbulo vamos manos a las sobras!!


  1. Primero que todo, vamos a empezar por crear la aplicación pop-client 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.popIngrese el nombre del Artifact:

    pop-client
    En las dependencias agregue:

    Web, Security y ThymeleafGenere 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-client
     0.0.1-SNAPSHOT
     pop-client
     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
    
    security.oauth2.resource.jwt.key-uri=http://localhost:8080/oauth/token_key
    security.oauth2.client.client-id=clientapp
    security.oauth2.client.client-secret=123456
    server.port=9000
    spring.http.converters.preferred-json-mapper=jackson
    spring.thymeleaf.cache=false
    

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

    oauth
    dashboard
  5. En el sub-package ouath vamos a crear una clase denominada JwkKeyPairManager, que será la encargada de manejar el keypair para firmar el atributo nonce del header y demostrar que se posee la clave privada (POP).

    package org.pigbar.hal9k.oauth2.pop.oauth;
    
    import java.security.KeyPair;
    import java.security.KeyPairGenerator;
    import java.security.NoSuchAlgorithmException;
    import java.security.interfaces.RSAPrivateKey;
    import java.security.interfaces.RSAPublicKey;
    
    import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
    import org.springframework.stereotype.Component;
    
    import com.nimbusds.jose.JWSAlgorithm;
    import com.nimbusds.jose.JWSHeader;
    import com.nimbusds.jose.JWSObject;
    import com.nimbusds.jose.Payload;
    import com.nimbusds.jose.crypto.RSASSASigner;
    import com.nimbusds.jose.jwk.JWK;
    import com.nimbusds.jose.jwk.RSAKey;
    
    @Component
    public class JwkKeyPairManager {
    
     private final JWK clientJwk;
    
     public JwkKeyPairManager() {
      KeyPair keyPair = createRSA256KeyPair();
      RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
      RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
      RandomValueStringGenerator random = new RandomValueStringGenerator();
      RSAKey.Builder builder = new RSAKey.Builder(publicKey);
      builder.keyID(random.generate());
      builder.privateKey(privateKey);
      this.clientJwk = builder.build();
     }
    
     public JWK createJWK() {
      return clientJwk.toPublicJWK();
     }
    
     public String getSignedContent(String content) {
      Payload contentPayload = new Payload(content);
    
      try {
       RSASSASigner rsa = new RSASSASigner(((RSAKey) clientJwk).toPrivateKey());
       JWSAlgorithm alg = JWSAlgorithm.RS256;
       JWSHeader header = new JWSHeader.Builder(alg).keyID(clientJwk.getKeyID()).build();
       JWSObject jws = new JWSObject(header, contentPayload);
       jws.sign(rsa);
       return jws.serialize();
      } catch (Exception e) {
       throw new RuntimeException(e);
      }
     }
    
     private KeyPair createRSA256KeyPair() {
      try {
       KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
       generator.initialize(2048);
       return generator.generateKeyPair();
      } catch (NoSuchAlgorithmException e) {
       throw new RuntimeException(e);
      }
     }
    
    }
    
  6.  Ahora, en el mismo paquete, vamos a crear un una clase denominada PoPTokenRequestEnhancer que se va a encargar de enviar la clave pública al servidor de autorización, Authorization Server, cuando se solicite un nuevo Token de acceso.

    package org.pigbar.hal9k.oauth2.pop.oauth;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpHeaders;
    import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
    import org.springframework.security.oauth2.client.token.AccessTokenRequest;
    import org.springframework.security.oauth2.client.token.RequestEnhancer;
    import org.springframework.stereotype.Component;
    import org.springframework.util.MultiValueMap;
    
    @Component
    public class PoPTokenRequestEnhancer implements RequestEnhancer {
    
        @Autowired
        private JwkKeyPairManager keyPairManager;
    
        @Override
        public void enhance(AccessTokenRequest request,
            OAuth2ProtectedResourceDetails resource,
            MultiValueMap<String, String> form,
            HttpHeaders headers) {
            form.add("public_key", keyPairManager.createJWK().toJSONString());
        }
    
    }
    
  7.  Como el cliente debe enviar el nonce firmado cada vez que interactúa con un recurso protegido, vamos a crear un Interceptor, HttpRequestWithPoPSignatureInterceptor, para agregar estos datos a cada endpoint externo con el que se trabaje. En el mismo paquete creamos lo siguiente:

    package org.pigbar.hal9k.oauth2.pop.oauth;
    
    import org.springframework.beans.BeansException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.http.HttpRequest;
    import org.springframework.http.client.ClientHttpRequestExecution;
    import org.springframework.http.client.ClientHttpRequestInterceptor;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.security.oauth2.client.OAuth2ClientContext;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.stereotype.Component;
    
    import java.io.IOException;
    import java.util.UUID;
    
    @Component
    public class HttpRequestWithPoPSignatureInterceptor
        implements ClientHttpRequestInterceptor, ApplicationContextAware {
    
        private ApplicationContext applicationContext;
    
        @Autowired
        private JwkKeyPairManager keyPairManager;
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
    
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution execution) throws IOException {
            OAuth2ClientContext clientContext = applicationContext.getBean(OAuth2ClientContext.class);
            OAuth2AccessToken accessToken = clientContext.getAccessToken();
    
            request.getHeaders().set("Authorization", "Bearer " + accessToken.getValue());
            request.getHeaders().set("nonce", keyPairManager.getSignedContent(UUID.randomUUID().toString()));
    
            return execution.execute(request, body);
        }
    
    }
    
  8. Como es necesario mantener o guardar los tokens, vamos a crear un servicio que se encargue de administrar los tokens del cliente o usuario. En el mismo paquete creamos el siguiente servicio:

    package org.pigbar.hal9k.oauth2.pop.oauth;
    
    import java.util.Date;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
    import org.springframework.security.oauth2.client.token.ClientTokenServices;
    import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.stereotype.Service;
    
    @Service
    public class OAuth2ClientTokenSevices implements ClientTokenServices {
    
        private ConcurrentHashMap<String, ClientUser> users = new ConcurrentHashMap<>();
    
        @Override
        public OAuth2AccessToken getAccessToken(OAuth2ProtectedResourceDetails resource, Authentication authentication) {
            ClientUser clientUser = getClientUser(authentication);
    
            if (clientUser.accessToken == null) return null;
    
            DefaultOAuth2AccessToken oAuth2AccessToken = new DefaultOAuth2AccessToken(clientUser.accessToken);
            oAuth2AccessToken.setAdditionalInformation(clientUser.additionalInformation);
            oAuth2AccessToken.setExpiration(new Date(clientUser.expirationTime));
    
            return oAuth2AccessToken;
        }
    
        @Override
        public void saveAccessToken(OAuth2ProtectedResourceDetails resource,
                Authentication authentication, OAuth2AccessToken accessToken) {
            ClientUser clientUser = getClientUser(authentication);
    
            clientUser.accessToken = accessToken.getValue();
            clientUser.expirationTime = accessToken.getExpiration().getTime();
            clientUser.additionalInformation = accessToken.getAdditionalInformation();
    
            users.put(clientUser.username, clientUser);
        }
    
        @Override
        public void removeAccessToken(OAuth2ProtectedResourceDetails resource,
                Authentication authentication) {
            users.remove(getClientUser(authentication).username);
        }
    
        private ClientUser getClientUser(Authentication authentication) {
            String username = ((User) authentication.getPrincipal()).getUsername();
            ClientUser clientUser = users.get(username);
    
            if (clientUser == null) {
                clientUser = new ClientUser(username);
            }
    
            return clientUser;
        }
    
        private static class ClientUser {
            private String username;
            private String accessToken;
            private Map<String, Object> additionalInformation;
            private long expirationTime;
    
            public ClientUser(String username) {
                this.username = username;
            }
        }
    }
    
  9. Procedamos ahora a crear la clase que se encargará de la configuración del cliente. Por lo tanto, en el paquete de costumbre, procedemos a crear:

    package org.pigbar.hal9k.oauth2.pop.oauth;
    
    import java.util.Arrays;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.oauth2.client.OAuth2ClientContext;
    import org.springframework.security.oauth2.client.OAuth2RestTemplate;
    import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
    import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
    import org.springframework.security.oauth2.client.token.ClientTokenServices;
    import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
    import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
    import org.springframework.security.oauth2.common.AuthenticationScheme;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
    
    @Configuration
    @EnableOAuth2Client
    public class ClientConfiguration {
    
     @Autowired
     private ClientTokenServices clientTokenServices;
    
     @Autowired
     private OAuth2ClientContext oauth2ClientContext;
    
     @Autowired
     private HttpRequestWithPoPSignatureInterceptor interceptor;
    
     @Autowired
     private PoPTokenRequestEnhancer requestEnhancer;
    
     @Bean
     public AuthorizationCodeResourceDetails authorizationCode() {
      AuthorizationCodeResourceDetails resourceDetails = new AuthorizationCodeResourceDetails();
    
      resourceDetails.setId("oauth2server");
      resourceDetails.setTokenName("oauth_token");
      resourceDetails.setClientId("clientapp");
      resourceDetails.setClientSecret("123456");
      resourceDetails.setAccessTokenUri("http://localhost:8080/oauth/token");
      resourceDetails.setUserAuthorizationUri("http://localhost:8080/oauth/authorize");
      resourceDetails.setScope(Arrays.asList("read_profile"));
      resourceDetails.setPreEstablishedRedirectUri(("http://localhost:9000/callback"));
      resourceDetails.setUseCurrentUri(false);
      resourceDetails.setClientAuthenticationScheme(AuthenticationScheme.header);
    
      return resourceDetails;
     }
    
     @Bean
     public OAuth2RestTemplate oauth2RestTemplate() {
      OAuth2ProtectedResourceDetails resourceDetails = authorizationCode();
      OAuth2RestTemplate template = new OAuth2RestTemplate(resourceDetails, oauth2ClientContext);
    
      AuthorizationCodeAccessTokenProvider authorizationCode = new AuthorizationCodeAccessTokenProvider();
      authorizationCode.setTokenRequestEnhancer(requestEnhancer);
    
      AccessTokenProviderChain provider = new AccessTokenProviderChain(Arrays.asList(authorizationCode));
    
      provider.setClientTokenServices(clientTokenServices);
      template.setAccessTokenProvider(provider);
      template.setInterceptors(Arrays.asList(interceptor));
    
      return template;
     }
    
    }
    
  10. Lista la configuración, procedemos entonces a definir los controles de acceso para este cliente. Nuevamente, en el mismo paquete, creamos lo siguiente:

    package org.pigbar.hal9k.oauth2.pop.oauth;
    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("/", "/index**", "/callback**", "/dashboard")
        .authenticated().anyRequest().authenticated();
     }
    }
    
  11.  Como esta es una aplicación cliente, es lógico que creemos algunos recursos con los que el usuario pueda interactuar. Los siguientes elementos se usan para definir un Dashboard de cliente. En el paquete dashboard procedemos a crear:

    Clase Entry
    package org.pigbar.hal9k.oauth2.pop.dashboard;
    
    public class Entry {
    
        private String value;
    
        public Entry(String value) {
            super();
            this.value = value;
        }
    
        public String getValue() {
            return value;
        }
    
    }
    
    Clase UserProfile
    package org.pigbar.hal9k.oauth2.pop.dashboard;
    public class UserProfile {
    
        private String name;
        private String email;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
    }
    
    Clase UserDashboard
    package org.pigbar.hal9k.oauth2.pop.dashboard;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.oauth2.client.OAuth2RestTemplate;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.client.HttpClientErrorException;
    import org.springframework.web.servlet.ModelAndView;
    
    import java.util.Arrays;
    import java.util.List;
    
    @Controller
    public class UserDashboard {
    
        @Autowired
        private OAuth2RestTemplate restTemplate;
    
        @GetMapping("/")
        public String home() {
            return "index";
        }
    
        @GetMapping("/callback")
        public ModelAndView callback(String code, String state) {
            return new ModelAndView("forward:/dashboard");
        }
    
        @GetMapping("/dashboard")
        public ModelAndView dashboard() {
            List<Entry> entries = Arrays.asList(
                    new Entry("entry 1"),
                    new Entry("entry 2"));
    
            ModelAndView mv = new ModelAndView("dashboard");
            mv.addObject("entries", entries);
    
            tryToGetUserProfile(mv);
            return mv;
        }
    
        private void tryToGetUserProfile(ModelAndView mv) {
            String endpoint = "http://localhost:8080/api/profile";
            try {
                UserProfile userProfile = restTemplate.getForObject(endpoint, UserProfile.class);
                mv.addObject("profile", userProfile);
            } catch (HttpClientErrorException e) {
                throw new RuntimeException("it was not possible to retrieve user profile");
            }
        }
    }
    
  12. Ahora, en la carpeta /pop-client/src/main/resources/templates creamos los siguientes archivos:

    Archivo index.html:
    <html xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>client app</title>
    </head>
    <body>
        <a href="/dashboard">Go to your dashboard</a>
    </body>
    </html>
    

    Archivo dashboard.html:
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
    <head>
    <title>client app</title>
    </head>
    <body>
      <h1>that's your dashboard</h1>
      <table>
        <tr>
          <td><b>That's your entries</b></td>
        </tr>
        <tr th:each="entry : ${entries}">
          <td th:text="${entry.value}">value</td>
        </tr>
      </table>
      <h3>your profile from [Profile Application]</h3>
      <table>
        <tr>
            <td><b>name</b></td>
            <td th:text="${profile.name}">username</td>
        </tr>
        <tr>
            <td><b>email</b></td>
            <td th:text="${profile.email}">email</td>
        </tr>
      </table>
    </body>
    </html>
    
  13. Finalmente, procedemos a crear nuestra clase de Application de springboot. Para ello nos dirigimos al paquete org.pigbar.hal9k.oauth2.pop y creamos la siguiente clase:

    package org.pigbar.hal9k.oauth2.pop;
    import javax.servlet.ServletContext;
    import javax.servlet.ServletException;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.web.servlet.ServletContextInitializer;
    
    @SpringBootApplication
    public class PopClientApplication implements ServletContextInitializer {
    
        public static void main(String[] args) {
            SpringApplication.run(PopClientApplication.class, args);
        }
    
        @Override
        public void onStartup(ServletContext servletContext)
                throws ServletException {
            servletContext.getSessionCookieConfig().setName("client-session");
        }
    }
    
Ya hemos creado nuestro cliente de OAUTH 2.0 con el mecanismo de Proof Of Possession of Key.
Ejecute el servidor. Ejecute este cliente. Vaya en el navegador a la URL http://localhost:9000 e introduzca sus credenciales, a saber pigbar y 1234 para el usuario y la clave respectivamente. Haga click en el enlace para el Dashboard...
 y veamos que sucede!!!

P.D.: Va a notar que debe hacer Login dos veces, una vez en el cliente y otra en el servidor. Como  evitar ese doble Login? Esta es precisamente su tarea a resolver!!! 

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