Autenticación con OpenID/OAuth en cualquier web con Nginx y de forma nativa con Spring Boot

Escrito por el .
java planeta-codigo programacion software-libre
Enlace permanente Comentarios

La autenticación permite identificar a los usuarios en una aplicación, en muchas es una necesidad para no permitir accesos no autorizados a la información que proporcionan o realizar las acciones que ofrecen. Las aplicaciones heredadas o legacy en ocasiones no es posible modificarlas y cuando una organización tiene varias aplicaciones gestionar los usuarios en cada una de ellas de forma individual se convierte en un problema. OpenID Connect proporciona la autenticación en el protocolo OAuth 2, con este protocolo las aplicaciones pueden delegar la autenticación a un proveedor de autenticación y ser este la que identifique a los usuarios y los gestione de forma forma centralizada además de proporcionar un inicio de sesión único o single-sing-on a varias aplicaciones. El servidor web Nginx tiene proxys que permiten añadir autenticación OAuth 2 a cualquier servicio web y las aplicaciones de Spring Boot pueden implementarlo de forma nativa.

OAuth

Java

La funcionalidad de autenticación y autorización es básica en la mayoría de aplicaciones web y servicios REST. La forma de implementar la autenticación y autorización es posible de varias formas. Sin embargo, con los métodos anteriores las aplicaciones gestionan sus propios usuarios y cada aplicación ha de implementar la autenticación y autorización. Otras formas comunes de autenticación son emplear autenticación básica en el servidor web, autenticación mutua entre el servidor y el cliente o con una implementación específica en la que una parte importante es guardar las contraseña de forma segura con salted-password-hashing.

Gestionar los usuarios de en cada una aplicación se convierte en un problema cuando el número de aplicaciones y servicios son varios, ya que a lo largo del tiempo hay que dar de alta a los usuarios nuevos y de baja a los usuarios que ya no deben acceder a la aplicación por ejemplo porque ya no forman parte de una organización. Por otro, lado hay aplicaciones heredadas o legacy que no se pueden modificar para añadirles la autenticación que necesitan.

Para administrar de forma centralizada los usuarios o credenciales de las aplicaciones y que el sistema de autenticación y autorización sea uno compartido para cualesquiera usuarios y aplicaciones una forma de implementarlo es usando un proveedor que implemente el estándar OpenID Connect. Para las aplicaciones heredadas la opción es añadir un proxy que proteja la aplicación con autenticación y en el caso de una aplicación Java con Spring Boot el proyecto de Spring Security permite añadir autenticación fácilmente para cualquier proveedor de OpenID.

Qué es OpenID Connect

OpenID Connect es una capa de identidad que funciona sobre el protocolo OAuth 2.0. Permite a los clientes verificar la identidad del usuario basándose en la autenticación realizada por el servidor de autorización, así como obtener información básica del perfil del usuario. El protocolo funciona con principios similares a REST lo que lo hace interoperable con cualquier sistema.

Son varios los proveedores que ofrecen autenticación con sus cuentas, algunos de ellos son Google, GitHub, Azure Active Directory, AWS Cognito u Okta. Para implementar el servicio de autenticación y autorización OAuth gestionando sin depender de esas otras organizaciones está Keycloak.

Autenticación OpenID/OAuth con Nginx

Para añadir autenticación OpenId Connect en una aplicación web se suele configurar con el servidor web actuando de proxy y un proxy de OAuth. La función del servidor web y el proxy es requerir que el usuario esté autenticado en el proveedor de autenticación. De este modo entre el usuario y la página web están el servidor web, el intermediario de OAuth y el proveedor de autenticación, el esquema es el siguiente.

Modos de funcionamiento de oauth2-proxy (con y sin Nginx)

Modos de funcionamiento de oauth2-proxy (con y sin Nginx)

Con el servidor web Nginx dos intermediarios o proxys que proporcionan autenticación OpenID Connect son oauth2-proxy y vouch-proxy. Tanto oauth2-proxy y vouch-proxy son dos servicios que le indican a Nginx si el usuario está autenticado usando la directiva auth_request de Nginx. Estos proxys simplemente devuelven como respuesta un código de estado 202 Accepted o 401 Unauthorized para indicarle a Nginx si hay que realizar la autenticación, las otras directivas de configuración son para establecer cabeceras con las que es posible entre otras cosas indicarle a la aplicación web final cual es el usuario autenticado. En caso de que haya que autenticar al usuario el proxy de OAuth redirige al proveedor de autenticación.

En el caso de oauth2-proxy la configuración consiste en hacer de proxy para la aplicación web en la ubicación / y requerir autenticación con el endpoint /oauth2/ y /oauth2/auth que hace de proxy para oauth2-proxy. Buena parte de esa configuración de proxy son el tratamiento de las cabeceras con las que se obtiene el nombre de usuario autenticado y correo electrónico.

Este es la configuración para Nginx.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    resolver 127.0.0.1;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    sendfile on;
    keepalive_timeout 65;

    include /etc/nginx/conf.d/*.conf;
}
nginx.conf
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
server {
    listen 80;
    server_name nginx.127.0.0.1.xip.io;
    root /var/www/html/;

    location /oauth2/ {
        proxy_pass http://oauth2:4180;

        proxy_set_header Host                    $host;
        proxy_set_header X-Real-IP               $remote_addr;
        proxy_set_header X-Scheme                $scheme;
        proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
    }
    
    location = /oauth2/auth {
        proxy_pass       http://oauth2:4180;

        proxy_pass_request_body           off;
        proxy_set_header Content-Length   "";

        proxy_set_header Host             $host;
        proxy_set_header X-Real-IP        $remote_addr;
        proxy_set_header X-Scheme         $scheme;
        proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
    }

    location / {
        auth_request /oauth2/auth;
        error_page 401 = /oauth2/start;

        auth_request_set $user   $upstream_http_x_auth_request_user;
        auth_request_set $email  $upstream_http_x_auth_request_email;
        auth_request_set $token  $upstream_http_x_auth_request_access_token;
        auth_request_set $auth_cookie $upstream_http_set_cookie;
        auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
        
        proxy_set_header X-User  $user;
        proxy_set_header X-Email $email;
        proxy_set_header X-Access-Token $token;

        add_header Set-Cookie $auth_cookie;

        if ($auth_cookie ~* "(; .*)") {
            set $auth_cookie_name_0 $auth_cookie;
            set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
        }

        if ($auth_cookie_name_upstream_1) {
            add_header Set-Cookie $auth_cookie_name_0;
            add_header Set-Cookie $auth_cookie_name_1;
        }

        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}
nginx.127.0.0.1.xip.io.conf

El servicio de proxy OAuth debe ubicarse en un subdominio de la página a autenticar ya que para esto se utilizan cookies.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
server {
    listen 80;
    server_name oauth2.nginx.127.0.0.1.xip.io;

    location /oauth2/callback {
        proxy_pass http://oauth2:4180;

        proxy_set_header Host                    $host;
        proxy_set_header X-Real-IP               $remote_addr;
        proxy_set_header X-Scheme                $scheme;
        proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;        
    }
    
    location / {
        return 302 http://nginx.127.0.0.1.xip.io;
    }
}
oauth2.nginx.127.0.0.1.xip.io.conf

Tanto oauth2-proxy como vouch-proxy tienen imágenes de Docker para su fácil uso sin necesidad de instalar nada en la máquina local salvo el propio Docker (imagen docker oauth2-proxy, imagen docker vouch-proxy). Ambos proxys requieren cierta configuración indicada en la documentación del archivo de configuración, las propiedades son desde el puerto en el que escucha el servicio del proxy las peticiones desde Nginx, configuración de TLS, proveedor de autenticación, dirección de redirección después de la autenticación, configuración de logging, por supuesto el client-id y el client-secret obtenidos del proveedor de autenticación, los correos electrónicos considerados como válidos y configuración de la cookie que mantiene la autenticación. También permite guardar la información de la sesión en Redis en vez de en una cookie.

La configuración del oauth-proxy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
http_address = "0.0.0.0:4180"

redirect_url = "http://oauth2.nginx.127.0.0.1.xip.io/oauth2/callback"

skip_provider_button = true
pass_host_header = true
pass_access_token = true
pass_authorization_header = true

upstreams = ["http://nginx.127.0.0.1.xip.io"]

provider = "google"
client_id = "949347437228-c2rqkjknbfq10jak1ugccfffocaut7vp.apps.googleusercontent.com"
client_secret = "AJUHi_m8bqCWI_FY9aHGLLcu"

authenticated_emails_file = "/etc/oauth2-proxy-authenticated-emails.cfg"

cookie_name = "_oauth2_proxy"
cookie_secret = "83L36igQrdxAWRxfajQXnzj8WMo9jNKe"
cookie_samesite = "lax"
cookie_secure = false
oauth2-proxy.cfg

Y en este caso las direcciones de correos electrónicas o usuarios permitidos en la aplicación.

1
2
pico.dev@gmail.com

oauth2-proxy-authenticated-emails.cfg

Al acceder a la página en el dominio nginx.127.0.0.1.xip.io se solicita el inicio de sesión o selección de cuenta.

Autenticación con cuenta de Google Autenticación con cuenta de Google

Autenticación con cuenta de Google

Una vez autenticado el usuario se permite el acceso a la página web, en este caso la página por defecto de Nginx, también se observa la creación de la cookie que mantiene la sesión.

Página web y cookie de sesión Página web y cookie de sesión

Página web y cookie de sesión

Al implementar el ejemplo me he encontrado con dos mensajes de error, OAuth2: unable to obtain CSRF cookie y http: named cookie not present. Para resolver el primero es necesario indicar el parámetro de configuración cookie-domain que en el momento de realizar el ejemplo solo me ha sido posible indicándolo a través de la línea de comandos no en el archivo de configuración y para resolver el segundo es necesario que el host del servicio proxy OAuth esté en un subdominio del dominio de la página web.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
version: '3'
services:
  oauth2:
    image: bitnami/oauth2-proxy:latest
    volumes:
      - "./oauth2-proxy.cfg:/etc/oauth2-proxy.cfg"
      - "./oauth2-proxy-authenticated-emails.cfg:/etc/oauth2-proxy-authenticated-emails.cfg"
    ports:
      - "4180:4180"
    command: oauth2-proxy --config=/etc/oauth2-proxy.cfg --cookie-domain="nginx.127.0.0.1.xip.io"

  nginx:
    image: nginx
    volumes:
      - "./nginx.conf:/etc/nginx/nginx.conf:ro"
      - "./oauth2.nginx.127.0.0.1.xip.io.conf:/etc/nginx/conf.d/oauth2.nginx.127.0.0.1.xip.io.conf:ro"
      - "./nginx.127.0.0.1.xip.io.conf:/etc/nginx/conf.d/nginx.127.0.0.1.xip.io.conf:ro"
    ports:
      - "80:80"
docker-compose.yml

Autenticación OpenID/OAuth con Apache

En el caso del servidor web Apache HTTPD la solución que he encontrado es usar el módulo mod_auth_openidc.

Autenticación OpenID en una aplicación Spring Boot

Las aplicaciones de Java que usan Spring Boot a través de la dependencia de Spring Security que soporta OpenID Connect con el que añadir soporte a la aplicación fácilmente con un proveedor de autenticación. En este ejemplo se usa Google como proveedor de autenticación , Keycloak es otro proveedor de autenticación OAuth para autenticar un servicio REST.

El siguiente ejemplo es un servicio de Spring Boot accedido desde el navegador, en caso de que el cliente fuese otra aplicación o servicio hay que obtener un token con el que poder invocar este servicio.

Primero hay que incluir en el proyecto la dependencia del cliente OAuth2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
plugins {
    id 'java'
    id 'application'
}

group = 'io.github.picodotdev.blogbitix.springoauth'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.springframework.boot:spring-boot-dependencies:2.4.0"))

    implementation('org.springframework.boot:spring-boot-starter')
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.springframework.boot:spring-boot-starter-oauth2-client')

    runtimeOnly('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtimeOnly('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}

application {
    mainClass = 'io.github.picodotdev.blogbitix.springoauth.Main'
}
build.gradle

Añadir la configuración en la aplicación para incluir el identificativo del cliente y el secreto del servicio de autenticación que permite validar la autenticación.

1
2
3
4
5
6
7
8
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 949347437228-c2rqkjknbfq10jak1ugccfffocaut7vp.apps.googleusercontent.com
            client-secret: AJUHi_m8bqCWI_FY9aHGLLcu
application.yml

La información del usuario autenticado se puede obtener con la anotación @AuthenticationPrincipal o mediante la clase SecurityContextHolder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package io.github.picodotdev.blogbitix.springoauth;

import java.util.Collections;
import java.util.Map;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;

@RestController
public class MainController {

    public MainController() {
    }

    @GetMapping("/")
    public String hello(@AuthenticationPrincipal OidcUser principal) {
        return String.format("Hello %s (%s)", principal.getUserInfo().getGivenName(), principal.getUserInfo().getEmail());
    }

    @GetMapping("/principal")
    public OidcUser getPrincipal(@AuthenticationPrincipal OidcUser principal) {
        return principal;
    }

    @GetMapping("/claims")
    public Map<String, Object> getClaims() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.getPrincipal() instanceof OidcUser) {
            OidcUser principal = ((OidcUser) authentication.getPrincipal());
            return principal.getClaims();
        }
        return Collections.emptyMap();
    }
}
MainController.java

Lo último necesario es configurar qué rutas necesitan autenticación con OpenID Connect, en caso de que el usuario no esté autenticado se realiza la redirección al proveedor de autenticación. Con la configuración indicada se requiere autenticación para acceder a todas las rutas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.github.picodotdev.blogbitix.springoauth;

import java.util.HashSet;
import java.util.Set;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;

@Configuration
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        Set<String> googleScopes = new HashSet<>();
        googleScopes.add("https://www.googleapis.com/auth/userinfo.email");
        googleScopes.add("https://www.googleapis.com/auth/userinfo.profile");
 
        OidcUserService googleUserService = new OidcUserService();
        googleUserService.setAccessibleScopes(googleScopes);
 
        http
          .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
          .oauth2Login(oauthLogin -> oauthLogin.userInfoEndpoint().oidcUserService(googleUserService));
    }
}
OAuth2LoginSecurityConfig.java

Al acceder a una de las URLs del cliente se solicita la autenticación con una cuenta de Google, una vez autenticado se accede a la página que en este caso obtiene la información del usuario como el nombre y correo electrónico.

Aplicación de Spring autenticada con una cuenta de Google

Aplicación de Spring autenticada con una cuenta de Google

Configuración de autenticación con cuentas de Google

En el ejemplo de este artículo muestro la autenticación con Google como proveedor de autenticación Oauth 2 pero perfectamente podría ser otro como Keycloak, en todos básicamente se trata de obtener las credenciales client-id y client-secret que permiten validar la autenticación tanto en estos casos Nginx como la aplicación de Spring Boot. Con una cuenta de Google y desde la consola para desarrolladores en el apartado credenciales es posible generar las credenciales para la autenticación de usuarios en una aplicación. Estas credenciales son los mencionados client-id y client-secret.

Los pasos para obtener las credenciales para una aplicación con autenticación OAuth con cuentas de Google son:

  • Crear un proyecto
  • Configurar la pantalla de consentimiento. En este paso se pide configurar la pantalla de consentimiento, la pantalla de consentimiento se muestra al usuario cuando realicen la autenticación entre la información está que aplicación solicita el consentimiento, opcionalmente es posible añadir un logotipo o enlaces de términos de uso.
  • Crear las credenciales del cliente. Aquí se configura las URL de retorno válidas cuando el usuario se haya autenticado. Si no se ha configurado la pantalla de consentimiento se solicita como paso previo a este.

Las credenciales tienen unos valores similares a, estas se indican tanto en la configuración de oauth-proxy como de la aplicación de Spring Boot:

  • client-id: 949347437228-m2c85v7bkmo1qb90vso702j27j4tccvr.apps.googleusercontent.com
  • client-secret: szgiplYPZR-pGgHP9MiJ-6-q

Al crear las credenciales para el cliente se indican las URL de retorno permitidas a las que se retorna al proxy o a la aplicación después del inicio de sesión.

Pasos para la creación de credenciales en Google para la autenticación OAuth

Pasos para la creación de credenciales en Google para la autenticación OAuth
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
docker-compose up

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: