Token con caducidad en Spring Security

3
28435

Token con caducidad en Spring Security

0. Índice de contenidos.

1. Introducción

Hoy en día ya sabemos que los mecanismos de autenticación de las distintas aplicaciones pueden ser tan complejos o simples como podamos imáginar. Tenemos la autenticación básica que ofrecen los propios navegadores, la comunmente utilizada de un formulario de usuario y contraseńa, contra LDAP o Active Directory, etc. Incluso podemos tener combinación de varios de estos mecanismos de seguridad o hacer extensiones de los mismos.

Por suerte, SpringSecurity nos ofrece ya una amplia variedad de mecanismos de autenticación que nos simplifican gran parte del trabajo. Así que sólo tenemos que configurarlos y/o hacer nuestras propias extensiones o modificaciones en caso de ser necesario.

El objetivo de este tutorial es la introducción de un token con un tiempo de caducidad para que el proceso de login se realice en un periodo de tiempo máximo. Esto es útil cuando tenemos mecanismos de autenticación automáticos, de forma que limitamos la ventana de tiempo para el acceso a nuestra aplicación.

Antes de seguir, tengo que dejar claro que esto por sí mismo no se puede considerar un mecanismo de autenticación, realmente es un complemento a un mecanismo de autenticación ya existente. Por lo que nuestro sistema será tan seguro como sea ese mecanismo de autenticación; y con este token de caducidad lo que hacemos es aumentar un poco su seguridad limitando el tiempo en el que permitimos acceder a la aplicación.

Como el mecanismo de autenticación más extendido es el de un formulario con usuario y contraseńa, voy a tomar éste como mecanismo base. Extenderemos esta forma de autenticación ańadiendo un token con caducidad que establezca un tiempo máximo para el proceso de login, desde que se inicia pidiendo la autenticación al usuario, hasta que se recibe de este su usuario y contraseńa.

2. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2 GHz Intel Core i7, 8GB DDR3 SDRAM).
  • Sistema Operativo: Mac OS X Lion 10.7.1
  • Spring Security 3.1.0.RC2
  • JSF 2.1.2

3. Proceso de autenticación en Spring Security.

Antes de nada, voy a explicar brévemente cómo funciona el proceso de autenticación en Spring Security. Este proceso se basa en un conjunto de interfaces y clases que interactúan conjuntamente en un orden establecido para terminar decidiendo si una petición tiene o no acceso. Spring realiza esto mediante una cadena de filtros en los que se irán haciendo las distintas acciones y comprobaciones necesarias hasta decidir si la autenticación es correcta o no. En este proceso podemos identificar básicamente a 3 actores principales:

  • AuthenticationFilter: Es el responsable de crear una instancia concreta del usuario y sus credenciales de autenticación.
  • AuthenticationManager: Responsable de la validación del usuario y credenciales y rellenar los permisos/roles que tiene el usuario o de lanzar los distintos tipos de excepciones en caso de fallo en la autenticación. Esto lo hace apoyándose en uno o varios proveedores de autenticación.
  • AuthenticationProvider: Es en quien se delega la correcta validación del usuario y recuperación de roles.

Este proceso a alto nivel sería:

  • 1.- El filtro de seguridad intercepta la petición de autenticación. Crea una instancia de Authentication para su validación.
  • 2.- El filtro de seguridad pasa al AuthenticationManager el objeto creado para que realice su validación. El AuthenticationManager delegará esta validación al proveedor o proveedores de autenticación.
  • 3.- El proveedor de autenticación comprobará si los datos de autenticación son correctos. En caso de haber algún tipo de error lanzará una excepción.
  • 4.- El filtro de seguridad comprueba el resultado de la autenticación solicitada al AuthenticationManager. Si se produjo una excepción redirigirá el flujo al AuthenticationFailureHandler y si hubo éxito al AuthenticationSuccessHanlder.

4. Configuración de nuestro mecanismo de autenticación.

Como sabéis, en Spring utilizamos un fichero de configuración. Así que en el caso de Spring Security no iba a ser distinto. Aquí os pongo el fichero de configuración que vamos a utilizar en nuestro ejemplo, luego pasaré a contar cuál es cada uno de los elementos del fichero relacionados con el proceso de autenticación.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
	
	<context:annotation-config />
	
	<aop:aspectj-autoproxy />
	
	<context:component-scan base-package="com.autentia.tutoriales" />
	
	<http access-denied-page="/_403.xhtml" auto-config="false"
		entry-point-ref="LoginUrlAuthenticationEntryPoint">
		<custom-filter ref="authenticationFilter" position="FORM_LOGIN_FILTER"></custom-filter>
		<intercept-url pattern="/*" access="IS_AUTHENTICATED_FULLY" />
	</http>

	<beans:bean id="LoginUrlAuthenticationEntryPoint"
		class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
		<beans:property name="loginFormUrl" value="/login.xhtml" />
	</beans:bean>

	<beans:bean id="authenticationFilter"
		class="com.autentia.tutoriales.UsernamePasswordWithTimeoutAuthenticationFilter">
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" />
		<beans:property name="authenticationFailureHandler" ref="authenticationFailureHandler" />
		<beans:property name="filterProcessesUrl" value="/login" />
		<beans:property name="usernameParameter" value="username" />
		<beans:property name="passwordParameter" value="passwword" />
		<beans:property name="timeoutParameter" value="timeout" />
	</beans:bean>
	
	<beans:bean id="authenticationFailureHandler"
		class="com.autentia.tutoriales.AutentiaAuthenticationFailureHandler">
		<beans:property name="defaultFailureUrl" value="/bad_credentials.html"/>
		<beans:property name="expiredUrl" value="/login_timeout.html"/>
	</beans:bean>
	
	<beans:bean id="authenticationSuccessHandler"
		class="com.autentia.tutoriales.AutentiaAuthenticationSuccessHandler">
	</beans:bean>

	
	<beans:bean id="authenticationProvider"
		class="com.autentia.tutoriales.AutentiaAuthenticationProvider">
		<beans:property name="nonceValiditySeconds" value="10"/>
		<beans:property name="key" value="KEY"/>
	</beans:bean>
	
	<authentication-manager alias="authenticationManager">
		<authentication-provider ref="authenticationProvider"/>
	</authentication-manager>
</beans:beans>

Respecto al mecanismo de seguridad en el fichero podemos ver:

  • Línea 16: Elemento que nos define cómo va a ser la autenticación de nuestra aplicación web por HTTP.
  • Línea 18: Establece la referencia al filtro de autenticación.
  • Línea 19: Se definen cuales són los recursos protegidos y quiénes tendrán acceso a ellos. En este caso se protege toda la aplicación y basta con que el usuario esté autenticado para que pueda acceder a los mismos.
  • Línea 22: Define cuál va a ser el punto de entrada para la autenticación. En este caso se redirige a la página /login.xhtml
  • Línea 27: Aquí definimos cual va a ser el filtro de autenticación responsable de formar un objeto con los parámetos recibidos por el usuario (username,password,timeout), pasarlo al AuthenticationManager para su validación y finalmente delegar el final del proceso dependiendo de si ha habido o no error de autenticación (authenticationSuccessHandler y authenticationFailureHandler).
  • Línea 38: Bean responsable de tratar los intentos de autenticación que han fallado y redirigir a la página de error.
  • Línea 44: Define cual va a ser el proveedor de autenticación responsable de comprobar que el usuario es válido y recuperar sus roles. En nuestro caso indicamos además el tiempo máximo para el proceso de login (nonceValiditySeconds) y la clave utilizada para generar una firma y evitar que el token pueda ser manipulado.
  • Línea 50: Indica que vamos a utilizar el AuthenticationManager por defecto de Spring y que este va a utilizar como proveedor de autenticación al que hemos definido en la línea 44.

5. Implementación del mecanismo de autenticación.

Pués como podéis ver en el fichero de configuración, tenemos alguna que otra clase que hay que implementar. El «EntryPoint» que utilizamos es el propio de Spring, así que lo primero que tenemos que implementar es el filtro. Como hemos dicho, vamos a hacer una extensión de la autenticación por usuario y contrase&nacute;a, así que como Spring ya nos ofrece un filtro para este tipo de autenticación, lo que hacemos es directamente heredar de él y sobreescribir aquello que nos haga falta. Básicamente lo realmente importante en el filtro es el método «attempAuthentication(..)». Éste será casi identico al de Spring salvo por un par de detalles, además de recuperar el usuario y la contrase&nacute;a, se debe recuperar el timeout y con esto formará un objeto de autenticación que ya lleve informado el timeout. Así que el filtro quedará:

package com.autentia.tutoriales;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class UsernamePasswordWithTimeoutAuthenticationFilter extends
		UsernamePasswordAuthenticationFilter {

	private String timeoutParameter = "timeout";
	private boolean postOnly;

	@Override
	public void setPostOnly(boolean postOnly) {
		super.setPostOnly(postOnly);
		this.postOnly = postOnly;
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !"POST".equals(request.getMethod())) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: "
							+ request.getMethod());
		}

		String username = obtainUsername(request);
		String password = obtainPassword(request);
		final String timeout = obtainTimeout(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		final UsernamePasswordWithTimeoutAuthenticationToken authRequest = new UsernamePasswordWithTimeoutAuthenticationToken(
				username, password, timeout);

		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}
	
	protected String obtainTimeout(HttpServletRequest request) {
        return request.getParameter(timeoutParameter);
    }

	public void setTimeoutParameter(String timeoutParameter) {
		this.timeoutParameter = timeoutParameter;
	}
}

Como véis el filtro crea una instancia de un token de autenticación que además de usuario y contrase&nacute;a tiene un timeout. Este token lo implementamos también heredando del token que ofrece Spring para usuario y contrase&nacute;a, de forma que lo único que hacemos es extenderlo para que tenga el timeout.

package com.autentia.tutoriales;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

public class UsernamePasswordWithTimeoutAuthenticationToken extends
		UsernamePasswordAuthenticationToken {

	private String timeout;

	public UsernamePasswordWithTimeoutAuthenticationToken(Object principal,
			Object credentials) {
		super(principal, credentials);
		this.timeout=null;
	}

	public UsernamePasswordWithTimeoutAuthenticationToken(Object principal,
			Object credentials, String timeout) {
		this(principal, credentials);
		this.timeout=timeout;
	}
	
	public String getTimeout() {
		return timeout;
	}	
}

Ahora nos vamos a encargar de los dos responsables del post-proceso de una autenticación con éxito o con error. Para esto también nos apoyamos en las clases de Spring de las cuáles extendemos nuestras propias clases.

En caso de éxito: (Se realizaría la lógica del intento de autenticación correcta y luego se redirige a una página de bienvenida)

package com.autentia.tutoriales;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Service;

@Service
public class AutentiaAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	public final void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		//lógica de tratamiento de autenticación correcta
		response.sendRedirect(response.encodeRedirectURL("welcome.xhtml"));
	}
}

Para el caso de un intento de autenticación errónea: (Se redirecciona a una página de error de autenticación o de timeout dependiendo del tipo de error)

package com.autentia.tutoriales;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.www.NonceExpiredException;

public class AutentiaAuthenticationFailureHandler extends
		SimpleUrlAuthenticationFailureHandler {
	
	private String defaultFailureUrl;
	private String expiredUrl;

	@Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
		final String failureUrl=getFailureUrl(exception);
        if (failureUrl == null) {
            logger.debug("No failure URL set, sending 401 Unauthorized error");

            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed: " + exception.getMessage());
        } else {
            saveException(request, exception);

            if (isUseForward()) {
                logger.debug("Forwarding to " + failureUrl);

                request.getRequestDispatcher(failureUrl).forward(request, response);
            } else {
                logger.debug("Redirecting to " + failureUrl);
                getRedirectStrategy().sendRedirect(request, response, failureUrl);
            }
        }
    }
    
	@Override
    public void setDefaultFailureUrl(String defaultFailureUrl) {
       super.setDefaultFailureUrl(defaultFailureUrl);
        this.defaultFailureUrl = defaultFailureUrl;
    }
	
	private String getFailureUrl(AuthenticationException exception) {
		if(exception instanceof NonceExpiredException){
			return expiredUrl;
		}
		return defaultFailureUrl;
	}

	public void setExpiredUrl(String expiredUrl) {
		this.expiredUrl = expiredUrl;
	}
}

Ahora ya sólo nos queda la implementación del proveedor de autenticación, que será el responsable de controlar que el token de caducidad sea correcto y que no se ha superado el timeout establecido. Para la generación y comprobación de este token, nos hemos basado en como Spring hace lo mismo para el tipo de autenticación DigestAuthentication, pero que no lo implementa en otros tipos de autenticación, por eso nos lo estamos teniendo que implementar nosotros.

La idea es generar un token de caducidad que no haga falta almacenar en el servidor para su posterior comprobación. Es decir, se meterá en un campo oculto del formulario y cuando éste se devuelva al servidor se comprobará su validez en el servidor. Para hacer esto tiene que ser autodefinido, es defir, debe contener la fecha de caducidad, y hay que protegerlo a posibles manipulaciones.

Así que nuestro proveedor de autenticación quedará de la siguiente forma:

package com.autentia.tutoriales;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.web.authentication.www.NonceExpiredException;
import org.springframework.stereotype.Service;

@Service
public class AutentiaAuthenticationProvider implements AuthenticationProvider {
	
	private static final Logger Log = LoggerFactory.getLogger(AutentiaAuthenticationProvider.class);

	private static final String NONCE_FIELD_SEPARATOR = ":";

	private String key = "KEY";
	
	private long nonceValiditySeconds=10;

	
	protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	@Override
	public final Authentication authenticate(Authentication authentication) {
		final UsernamePasswordWithTimeoutAuthenticationToken authenticationToken = (UsernamePasswordWithTimeoutAuthenticationToken)authentication;
		validateTimeout(authenticationToken);
		//lógica de comprobación de usuario y contrase&nacute;a
		return createSuccessAuthentication(authenticationToken);
	}
	
	@Override
	public final boolean supports(Class<?> authentication) {
		return UsernamePasswordWithTimeoutAuthenticationToken.class.isAssignableFrom(authentication);
	}
	
	public long getNonceValiditySeconds() {
		return nonceValiditySeconds;
	}

	public void setNonceValiditySeconds(long nonceValiditySeconds) {
		this.nonceValiditySeconds = nonceValiditySeconds;
	}
	
	public String getKey() {
		return key;
	}

	public void setKey(String key) {
		this.key = key;
	}

	private void validateTimeout(
			UsernamePasswordWithTimeoutAuthenticationToken authenticationToken) {
		if(StringUtils.isEmpty( authenticationToken.getTimeout())){
			final String msg="Timeout signature not present.";
			Log.error(msg);
			throw new BadCredentialsException(msg);
		}
		final long timeOutTime=extractNonceValue(authenticationToken.getTimeout());
		
		if (isNonceExpired(timeOutTime)){
			final String msg="Login timeout";
			Log.error(msg);
			throw new NonceExpiredException(msg);
		}
	}
	
	boolean isNonceExpired(final long timeoutTime) {
        final long now = System.currentTimeMillis();
        return timeoutTime 

Como se puede ver, el método "calculateNonce()" es el que calcula el valor del token de seguridad que se enviará al formulario Web del cliente. Este valor se calcula por composición de dos partes y se codifica en Base64. Las dos partes del token son:

  • Fecha máxima para el login: Cálculo con el tiempo actual en milisegundos más el valor de los segundos definidos como periodo de login
  • Firma del servidor para evitar manipulación: Se compone de un String con la fecha anteriormente calculada en milisegundos, más una clave propia del servidor sobre la que se hace un digest en MD5 en formato hexadecimal.

A la hora de recibir el formulario con el intento de autenticación, lo primero que se hace es validar el timeout con el método «validateTimeout()», donde se recuperará el valor de la fecha máxima para el proceso de login y luego que aún no se ha superado. En el método «extractNonceValue(…)» se comprueba que éste parámetro no ha sido manipulado y que está correctamente informado, en caso contrario se lanza la excepción «BadCredentialsException». Por último, ya solo basta comprobar que no se ha superado el tiempo para el proceso de login, método «isNonceExpired(…)», que en caso de haberse superado se lanza la excepción «NonceExpiredException».

Ya sólo nos falta crear el formulario de autenticación al que le pasaremos el cálculo de este token de caducidad. Primero creamos la página del formulario:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html">
<h:head>
	<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
	<title><h:outputText value="Token con caducidad en Spring Security" /></title>
</h:head>
<h:body>
		<div id="login">
			<form id="loginForm" action="login" method="post">
				<input type="hidden" name="username" value="" /> 
				<input type="hidden" name="password" value="" />
				<h:inputHidden id="timeout" value="#{loginTimeOutView.nonce}"/>
				<input type="submit" value="Entrar"/>
			</form>
		</div>
</h:body>
</html>

Como podéis ver en el campo oculto «timeout» hemos metido el valor que nos devuelve el controlador «LoginTimeout» en su método «getNonce(…)». Este será de la siguiente forma:

package com.autentia.tutoriales;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.RequestScoped;

@ManagedBean
@RequestScoped
public class LoginTimeOutView {
	
	@ManagedProperty("#{autentiaAuthenticationProvider}")
	private transient AutentiaAuthenticationProvider authenticationProvider;
	
	public void setAuthenticationProvider(
			AutentiaAuthenticationProvider authenticationProvider) {
		this.authenticationProvider = authenticationProvider;
	}

	public String getNonce(){
		return authenticationProvider.calculateNonce();
	}
}

Aquí podéis ver cómo en este controlador se inyecta el proveedor de autenticación (AutentiaAuthenticationProvider) para pedirle que genere el token de caducidad que luego él mismo deberá conprobar cuando se le envíe el formulario.

6. Conclusiones

Bueno, pues con este ejemplo se puede comprobar que gracias a la arquitectura de autenticación que tiene Spring Security, y a la implementación por defecto que ya trae, podemos realmente hacer muchas cosas en apenas unos pocos pasos. Como en el ejemplo propuesto, a&nacute;adiendo un token de caducidad generado en el servidor para el proceso de login.

Ya sabeís que esto es un ejemplo básico de lo que podemos hacer, pero espero que os sirva a alguno en vuestros casos particulares.

Saludos.

3 COMENTARIOS

  1. Donde se puede descargar el codigo? ya que hay una parte donde se define el AutentiaAuthenticationProvider que si te fijas abajo esta incompleta y aparece codigo html, Es posible integrar este provider usando el ya existente en Spring DaoAuthenticationProvider para poder usar las ventajas del password encoder.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad