1. Introducción

En este post vamos a configurar un servidor OAuth en Spring Boot. OAuth es un protocolo que permite la integración entre sistemas sin comprometer las credenciales de los usuarios. En el proceso de autenticación OAuth intervienen dos agentes. El servidor OAuth en donde se encuentra la información del usuario que puede ser accedida a través de apis y el cliente OAuth que desea integrar la información de los usuarios del servidor en su sistema. El proceso lo inicia el cliente solicitando al usuario permisos para consultar su información. El resultado de dicho proceso será la obtención de los tokens de acceso y refresco que permitirán finalmente consultar la información del usuario a través de las apis expuestas en el servidor.

La gran ventaja de OAuth es que las credenciales de los usuarios en el servidor no se ven comprometidas ya que el acceso a su información se hace a través de tokens que deberán ser validados cuando consumimos las apis.

Os podéis descargar el código de ejemplo de mi GitHub aquí.

Tecnologías empleadas:

  • Java 8
  • Gradle 3.1
  • SpringBoot 1.5.2.RELEASE
  • Spring 4.3.7.RELEASE
  • SpringOAuth2 2.2.0.13.RELEASE

2. Creación de microservicio de ejemplo y securización

Lo primero que vamos a hacer es crear un microservicio de ejemplo en donde expondremos nuestra api que securizaremos utilizando Spring Security.

Añadimos las dependencias org.springframework.boot:spring-boot-starter-web, org.springframework.boot:spring-boot-starter-security, org.springframework.security.oauth:spring-security-oauth2 al fichero build.gradle

group 'com.jorgehernandezramirez.spring.springboot.oauth.server'
version '1.0-SNAPSHOT'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'maven'
apply plugin: 'spring-boot'
apply plugin: 'io.spring.dependency-management'

dependencyManagement {
    imports {
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.SR6'
    }
}

sourceCompatibility = 1.8

springBoot {
    mainClass = "com.jorgehernandezramirez.spring.springboot.oauth.server.Application"
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.security.oauth:spring-security-oauth2")
}

Main que arranca SpringBoot.

package com.jorgehernandezramirez.spring.springboot.oauth.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public Application(){
        //For Spring
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Creamos los controladores ResourceController y UserController de test.

package com.jorgehernandezramirez.spring.springboot.oauth.server.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResourceController {

    public ResourceController(){
        //Para Spring
    }

    @RequestMapping("/public")
    public String getPublicResource(){
        return "Public Resource";
    }

    @RequestMapping("/private")
    public String getResource(){
        return "Private Resource";
    }

    @RequestMapping("/admin")
    public String getAdminResource(){
        return "Admin resource";
    }

    @RequestMapping("/admin/oauth")
    public String getAdminOAuth(){
        return "Admin OAuth resource";
    }
}
package com.jorgehernandezramirez.spring.springboot.oauth.server.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
public class UserController {

    public UserController(){
        //Para Spring
    }

    @RequestMapping("/user")
    public Principal getPublicResource(final Principal principal){
        return principal;
    }
}

A continuación procedemos a securizar la aplicación. Para ello creamos los siguientes usuarios en memoria

  • admin
  • jorge

y asignamos para cada url los siguientes permisos

  • La url /public, /oauth/token y /oauth/authorize son públicas
  • Para acceder a /admin el usuario deberá tener el rol ADMIN
  • Para acceder a todas las demás urls se deberá estar autenticado

PD: Las urls /oauth/token y /oauth/authorize son las que intervendrán en el proceso de autenticación oauth.

package com.jorgehernandezramirez.spring.springboot.oauth.server.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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 SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .headers().frameOptions().disable()
                .and().authorizeRequests()
                .antMatchers("/oauth/token", "/oauth/authorize**", "/public").permitAll()
                .antMatchers("/admin").access("hasRole('ROLE_ADMIN')")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("admin").roles("ADMIN").and()
                .withUser("jorge").password("jorge").roles("USER");
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

3. Configuración de servidor OAuth

Para configurar el servidor OAuth dentro de nuestro microservicio de SpringBoot deberemos configurar el servidor de autorización y de recursos.

  • Servidor de autorización. Permite configurar los clientes oauth que son necesarios para realizar el proceso de autenticación
  • Servidor de recursos. Permite securizar los recursos que van a ser accedidos a través de los token de acceso resultado del proceso de autenticación

Authorization Server

Para configurar un cliente oauth deberemos especificar:

  • withClient. Id del cliente
  • secret. Password del cliente
  • authorizedGrantTypes. Tipo de permisos que le permitirán realizar operaciones en el proceso de autenticación oauth así como en la generación de tokens
  • scopes. Ámbitos o conjunto de recursos, que los usuarios que se integran vía oauth, conceden al cliente para consultar sus datos.
  • redirectUris. Url a la que se redirige una vez el proceso de autenticación oauth haya finalizado.
  • accessTokenValiditySeconds. Tiempo en el que el access token es válido
  • refreshTokenValiditySeconds. Tiempo en el que el refresh token es válido
package com.jorgehernandezramirez.spring.springboot.oauth.server.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler;
import org.springframework.security.oauth2.provider.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    private static String REALM="MY_OAUTH_REALM";

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private UserApprovalHandler userApprovalHandler;

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("my-trusted-client")
                .secret("secret")
                .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit", "client_credentials")
                .scopes("read")
                .redirectUris("http://example.com")
                .accessTokenValiditySeconds(300)
                .refreshTokenValiditySeconds(6000);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
                .authenticationManager(authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.realm(REALM + "/client");
    }

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Bean
    @Autowired
    public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
        TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
        handler.setTokenStore(tokenStore);
        handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
        handler.setClientDetailsService(clientDetailsService);
        return handler;
    }

    @Bean
    @Autowired
    public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
        TokenApprovalStore store = new TokenApprovalStore();
        store.setTokenStore(tokenStore);
        return store;
    }
}

Resource Server

package com.jorgehernandezramirez.spring.springboot.oauth.server.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.error.OAuth2AccessDeniedHandler;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "my_rest_api";

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).stateless(false);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.
                anonymous().disable().
                headers().frameOptions().disable()
                .and().requestMatchers().antMatchers("/private", "/admin", "/admin/oauth", "/user")
                .and().authorizeRequests()
                .antMatchers("/private", "/user").authenticated()
                .antMatchers("/admin").access("hasRole('ROLE_ADMIN')")
                .antMatchers("/admin/oauth").access("#oauth2.hasScope('read')")
                .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
    }
}

Asignamos el orden para el filtro del servidor de recursos a 3. Según la release de Spring-Boot-1.5 se cambió de 3 a SecurityProperties.ACCESS_OVERRIDE_ORDER - 1 en esta versión. Es necesario cambiarlo a 3 ya que al acceder a las apis del servidor a través del token de acceso es necesario que este filtro salte antes que el filtro de seguridad por defecto.

security:
   oauth2:
      resource:
         filter-order: 3

4. Probando la aplicación

El proceso de autenticación oauth empieza mostrándole al usuario el formulario de login de la aplicación que queremos integrar. Debemos ejecutar la siguiente url en el navegador indicando los siguientes parámetros

  • El id del cliente
  • La url de redirección

El resultado de este proceso será la obtención del parámetro code.

http://localhost:8080/oauth/authorize?response_type=code&client_id=my-trusted-client&redirect_uri=http://example.com

Procedemos a logarnos con un usuario del sistema

Se nos muestra la pantalla de aprobación en donde el usuario concede permisos al cliente my-trusted-client para acceder a sus recursos.

Finalmente el proceso nos lleva a la url de redirección asociada a nuestro cliente pasando el parámetro code.

Obtener access y refresh token

A continuación obtenemos los token de acceso y refresco a partir del parámetro code obtenido en el paso anterior. La diferencia entre ellos es que el token de acceso tiene generalmente un tiempo de vida más corto y se utiliza para acceder a las apis del sistema, mientras que el token de refresco tiene un tiempo de vida más largo y se utiliza para generar otros token de acceso.

curl -v -X POST http://localhost:8080/oauth/token --data "grant_type=authorization_code&client_id=my-trusted-client&redirect_uri=http://example.com&code=<strong>PUThQB</strong>" --user my-trusted-client:secret -H "Accept:application/json"

Resultado

{“access_token”:”20b5c304-d321-4acd-9452-a8df67a07c36″,”token_type”:”bearer”,”refresh_token”:”f47bf0c8-f7a8-4b4f-9e76-3e64fa71b202″,”expires_in”:299,”scope”:”read”}

Atacando la api

Podemos consumir la api del servidor de dos maneras. Pasándo el token de acceso en el parámetro access_token

curl -X GET http://localhost:8080/admin?access_token=20b5c304-d321-4acd-9452-a8df67a07c36

o pasando el token de acceso en la cabecera de autorización Authorization.

curl -v -X GET http://localhost:8080/admin-H "Authorization:Bearer 20b5c304-d321-4acd-9452-a8df67a07c36"

Resultado

Admin resource

Refrescando el token

El token de refresco sirve para generar otros token de acceso.

curl -v -X POST http://localhost:8080/oauth/token --data "grant_type=refresh_token&refresh_token=f47bf0c8-f7a8-4b4f-9e76-3e64fa71b202" --user my-trusted-client:secret -H "Accept:application/json"

{“access_token”:”ada3866e-19b3-4af2-820a-55e8c68dbbfd”,”token_type”:”bearer”,”refresh_token”:”f47bf0c8-f7a8-4b4f-9e76-3e64fa71b202″,”expires_in”:300,”scope”:”read”}