Integrar autenticación OAuth con Keycloak, Shiro, Apache Tapestry y Spring Boot

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

OAuth es un protocolo usado para permitir a una aplicación acceder a los recursos de un usuario sin que este proporcione a la aplicación cliente sus credenciales y manteniendo el control de revocar los permisos concedidos. Es ampliamente usado por los servicios de redes sociales de las empresas más conocidas, también lo podemos usar en nuestras aplicaciones. En el ejemplo usaré Keycloak y una aplicación Java con Spring Boot, Apache Shiro y Apache Tapestry.

Keycloak

Java

El protocolo OAuth permite a una aplicación cliente acceder a los recursos de un usuario almacenados en otra aplicación sin que el usuario proporcione a la aplicación cliente sus credenciales, además el usuario tiene la capacidad de revocar en caulquier momento los permisos concedidos a la aplicación cliente. El protocolo OAuth es ampliamente usado por empresas como Google, Facebook, Twitter en sus aplicaciones y servicios. También aplicando una arquitectura de microservicios, al dividir una aplicación en varios módulos o simplemente varias aplicaciones independientes pero que son usadas al mismo tiempo por el mismo usuario para evitar que el usuario se autentique en cada aplicación individualmente y que cada aplicación implemente la funcionalidad de autenticación podemos centralizarla usando OAuth a modo de autenticación única o SSO. Usando Keycloak como servidor de OAuth podemos integrarlo en una aplicación Java que use Apache Shiro para la autorización, Spring Boot para iniciar la aplicación y Apache Tapestry como framework web.

En el protocolo OAuth se diferencia las aplicaciones cliente que son capaces de mantener seguras sus credenciales como es el caso de una aplicación web ejecutada en el servidor o las aplicaciones que no son capaces de mantener sus credenciales seguras como es el caso de una aplicación cliente ejecutada en el navegador o en algunos casos nativa en el móvil. Independientemente de la aplicación cliente o de los varios flujos de autenticación el acceso a los recursos del usuario se hace mediante la obtención de un token que es una cadena de caracteres opaca de cierta longitud pero que descifrada contiene información del usuario autenticado también está firmada digitalmente por el servidor de OAuth para evitar alteraciones. El protocolo define varios flujos para obtener un token, obtenido el token con cualquiera de ellos el acceso a los recursos es indiferente del flujo que haya sido empleado.

En una aplicación segura con el grant de tipo authorization code los pasos que se siguen son los siguientes:

  • El servidor redirige al usuario al servidor de OAuth cuando intenta acceder a una URL protegida.
  • El usuario introduce sus credenciales en una página de inicio de sesión proporcionada por el servidor OAuth, normalmente un usuario y contraseña.
  • El servidor OAuth envía al navegador una redirección hacia la aplicación proporcionado un código de autorización en la URL que puede intercambiarse por un token.
  • El navegador con la redirección envía el código de autorización al servidor, el servidor obtiene de la URL, obtiene el código de autorización y lo usa para intercambiarlo por un token del servidor OAuth proporcionado además las credenciales del cliente.
  • Obtenido el token con los permisos adecuados la aplicación ya puede permitir acceso o acceder a los recursos.

Para obtener el token el servidor mantiene seguras sus credenciales como cliente OAuth. Nótese también que con el token el servidor (cliente OAuth) no necesita comunicarse con el servidor OAuth para validar el token ya que está firmado digitalmente, cifrado y tiene concedido un periodo de validez.

Un cliente se considera inseguro si la aplicación cliente no puede mantener seguras sus credenciales, si las credenciales de la aplicación están en el navegador o en una aplicación nativa del móvil se considera que las credenciales podrían obtenerse. En una aplicación web en un servidor las credenciales de la aplicación se mantienen seguras en el servidor.

El siguiente ejemplo muestra como autenticar con Keycloak como proveedor de OAuth una aplicación Java que usa Shiro para la autorización, Spring Boot y el framework web Apache Tapestry. OAuth y Keycloak también puede usarse para securizar con OAuth un servicio REST con JAX-RS y crear un cliente Java para acceder al servicio REST securizado con OAuth empleando el flujo client credentials. Lo mostrado en este artículo solo es una pequeña parte de las opciones y posibilidades que ofrece Keycloak, en las capturas de pantalla mostradas hay muchas pestañas, opciones y campos con funcionalidades adicionales.

Iniciar el servidor OAuth de Keycloak usando Docker es muy sencillo con el siguiente comando y archivo de Docker Compose, en el primer acceso se nos solicitará una clave y contraseña de administración:

1
2
3
4
5
6
7
keycloak:
    image: jboss/keycloak
    ports:
        - 9080:8080
    environment:
        - KEYCLOAK_USER=admin
        - KEYCLOAK_PASSWORD=admin
docker-compose.yml
1
2
$ docker-compose up

docker-compose.sh

Para el ejemplo crearé un nuevo realm que contendrá los usuarios y en el que registraremos la aplicación cliente.

Keycloak realm Keycloak client

Keycloak client roles Keycloak roles

Keycloak users Keycloak users role mappings

Usando uno de los adaptadores proporcionados por Keycloak para la integración en servidores y aplicaciones su uso no es complicado, en este caso usaré el adaptador para Spring Boot. Usándolo básicamente deberemos proporcionar en la configuración las credenciales de la aplicación cliente que hemos registrado previamente en Keycloak. Además indicaremos que URLs de la aplicación requiere autenticación y que roles han de poseer los usuarios autenticados. Al acceder a estas URLs el adaptador de Keycloak redirigirá al servidor para que el usuario se autentique, una vez autenticado se redirigirá a la aplicación de nuevo.

 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
server:
    port: 8443
    ssl:
        key-store: classpath:keystore.jks
        key-store-password: secret
        key-password: secret

management:
    port: 8090
    context-path: '/management'
    
endpoints:
    metrics:
        sensitive: true
    shutdown:
        enabled: true

keycloak:
    realm: keycloak
    realmKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArx9/6b9koplCBta5vy0/GEYVUYaYSUcHIiTyE2utfSUlfL8Px//0haNgk5FFfeTuSa/Ha2mO+kP5qTBN5Muov+1ytONrkbGOSCkOH9rVRjsZfpdW3Z+wYdZ8tAiEzUcjbCYxPszgT00Pxr/NJ6nLJoPT5s+8osl4c2j0JyR2qYV9e7loeJs2ciJe/ZzHNptz4JrExhzxTJGjo6ZNuBtIfyoK6EFA4VKzj2152FrQSYIafOmTBM/42cyd2kyxx04TogCZYzj7Pe78aT6uxPoGsk8PK1YkAtINROJVqJZsLTsso2kB9R8UkjF1MEYHPrdoXVveeLv/1Ci6uWNbgDMCywIDAQAB
    auth-server-url: http://localhost:9080/auth
    ssl-required: external
    resource: web
    credentials.secret: 6db55aa0-5466-4432-aca7-97c2ab2246ee
    use-resource-role-mappings: true
    securityConstraints[0]:
        securityCollections[0]:
            name: user
            authRoles:
                - user
            patterns:
                - /user
    securityConstraints[1]:
        securityCollections[0]:
            name: admin
            authRoles:
                - admin
            patterns:
                - /admin
application.yml

Autenticado el usuario podemos obtener la instancia de AccessToken que representa el token de OAuth, para la autorización podemos usar Apache Shiro y para ellos deberemos implementar un Realm de tipo AuthorizingRealm. Tiene dos métodos que deberemos implementar doGetAuthenticationInfo y doGetAuthorizationInfo, el primero lo usaremos para autenticar al usuario que en este caso teniendo el AccessToken ya estará autenticado con Keycloak y el segundo método nos permitirá obtener los roles y permisos asociados al usuario que podríamos obtenerlos de una base de datos relacional, en el ejemplo los roles también se obtienen del token. Con un filtro realizaremos el inicio de sesión de forma programática del usuario representado por el AccessToken cuando esté presente en la petición.

 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
package io.github.picodotdev.keycloak.services;

import org.apache.shiro.SecurityUtils;
import org.apache.tapestry5.services.HttpServletRequestFilter;
import org.apache.tapestry5.services.HttpServletRequestHandler;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.representations.AccessToken;
import org.tynamo.security.federatedaccounts.oauth.tokens.OauthAccessToken;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class KeycloakFilter implements HttpServletRequestFilter {

    @Override
    public boolean service(HttpServletRequest request, HttpServletResponse response, HttpServletRequestHandler handler) throws IOException {
        AccessToken accessToken = getAccessToken(request);
        if (accessToken != null) {
            SecurityUtils.getSubject().login(new OauthAccessToken(accessToken, accessToken.getExpiration()));
        }
        return handler.service(request, response);
    }

    private AccessToken getAccessToken(HttpServletRequest request) {
        if (!(request.getUserPrincipal() instanceof KeycloakPrincipal)) {
            return null;
        }
        KeycloakPrincipal principal = (KeycloakPrincipal) request.getUserPrincipal();
        return principal.getKeycloakSecurityContext().getToken();
    }
}
KeycloakFilter.java
 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
package io.github.picodotdev.keycloak.services;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.keycloak.representations.AccessToken;
import org.tynamo.security.federatedaccounts.oauth.tokens.OauthAccessToken;

public class AppRealm extends AuthorizingRealm {

    public AppRealm() {
        super(new MemoryConstrainedCacheManager());

        setAuthenticationTokenClass(OauthAccessToken.class);
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        AccessToken accessToken = (AccessToken) authenticationToken.getPrincipal();
        return new SimpleAuthenticationInfo(accessToken, accessToken, accessToken.getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        AccessToken accessToken = (AccessToken) principalCollection.getPrimaryPrincipal();
        return new SimpleAuthorizationInfo(accessToken.getRealmAccess().getRoles());
    }
}
AppRealm.java

Con Apache Tapestry el filtro se registra en el módulo de la aplicación y con Shiro podemos realizar la autorización necesaria en las páginas u acciones de la aplicación usando anotaciones. En este caso una página pública que no requiere estar autenticado, una página accesible por un usuario autenticado y con rol user y finalmente una página de administración que requiere rol admin.

1
2
3
4
package io.github.picodotdev.keycloak.pages;

public class Index {
}
Index.java
1
2
3
4
5
6
7
package io.github.picodotdev.keycloak.pages;

import org.apache.shiro.authz.annotation.RequiresUser;

@RequiresUser
public class User {
}
User.java
1
2
3
4
5
6
7
package io.github.picodotdev.keycloak.pages;

import org.apache.shiro.authz.annotation.RequiresRoles;

@RequiresRoles("admin")
public class Admin {
}
Admin.java
 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
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<head>
	<title>OAuth, Keycloak, Apache Shiro, Apache Tapestry y Spring Boot</title>
	<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,700&amp;subset=latin,latin-ext" rel="stylesheet" type="text/css"/>
	<link href="http://fonts.googleapis.com/css?family=Cantarell:400,700" rel="stylesheet" type="text/css"/>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="jumbotron">
                    <img t:type="any" src="context:images/keycloak.png"/>
                    <img t:type="any" src="context:images/apache-tapestry.png"/>
                </div>
            </div>
         </div>
        <div class="row">
            <div class="col-md-12">
                <t:security.guest>
                    ¡Hola invitado!
                </t:security.guest>

                <t:security.user>
                    ¡Hola usuario!
                </t:security.user>

                <t:security.hasRole role="admin">
                    ¡Hola administrador!
                </t:security.hasRole>

                <t:pagelink page="user">Página de usuario</t:pagelink>, <t:pagelink page="admin">Página de administrador</t:pagelink>
            </div>
         </div>
    </div>
</body>
</html>
Index.tml
 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
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<head>
	<title>Keycloak</title>
	<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,700&amp;subset=latin,latin-ext" rel="stylesheet" type="text/css"/>
	<link href="http://fonts.googleapis.com/css?family=Cantarell:400,700" rel="stylesheet" type="text/css"/>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="jumbotron">
                    <img t:type="any" src="context:images/keycloak.png"/>
                    <img t:type="any" src="context:images/apache-tapestry.png"/>
                </div>
            </div>
         </div>
        <div class="row">
            <div class="col-md-12">
                ¡Hola usuario!, <t:pagelink page="index">página de inicio</t:pagelink>
            </div>
         </div>
    </div>
</body>
</html>
User.tml
 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
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<head>
	<title>Keycloak</title>
	<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,700&amp;subset=latin,latin-ext" rel="stylesheet" type="text/css"/>
	<link href="http://fonts.googleapis.com/css?family=Cantarell:400,700" rel="stylesheet" type="text/css"/>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="jumbotron">
                    <img t:type="any" src="context:images/keycloak.png"/>
                    <img t:type="any" src="context:images/apache-tapestry.png"/>
                </div>
            </div>
         </div>
        <div class="row">
            <div class="col-md-12">
                ¡Hola administrador!, <t:pagelink page="index">página de inicio</t:pagelink>
            </div>
         </div>
    </div>
</body>
</html>
Admin.tml

Como la página de inicio no requiere autenticación es accesible por cualquier usuario. Al navegar a la página de usuario o administrador se iniciará el proceso de autenticación primeramente redirigiéndonos al servidor Keycloak para que introduzcamos las credenciales.

Index Login

User

Si intentamos acceder a la página de usuario o administrador sin estar autenticados se nos mostrará la página de error 403 y al acceder a la página de administración con un usuario sin rol admin se nos mostrará la página de error 401.

Error 403

Un buen libro sobre OAuth que he leído es Mastering OAuth 2.0 que explica detalladamente el protocolo OAuth junto con el resto de formas de obtener un token además del mostrado en este artículo usando las credenciales del cliente.

Este artículo solo es introductorio a las posibilidades de OAuth y Keycloak, entre otras posibilidades que ofrece Keycloak creo que está permitir registrarse a los usuarios o personalizar los estilos y páginas de autenticación.

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:
./gradle run

Portada libro: PlugIn Tapestry

Libro PlugIn Tapestry

Si te interesa Apache Tapestry descarga gratis el libro de más de 300 páginas que he escrito sobre este framework en el formato que prefieras, PlugIn Tapestry: Desarrollo de aplicaciones y páginas web con Apache Tapestry, y el código de ejemplo asociado. En el libro comento detalladamente muchos aspectos que son necesarios en una aplicación web como persistencia, pruebas unitarias y de integración, inicio rápido, seguridad, formularios, internacionalización (i18n) y localización (l10n), AJAX, ... y como abordarlos usando Apache Tapestry.



Comparte el artículo: