Anteriormente utilizábamos un token generado por POSE, pero se ha cambiado a OAuth, a partir del 21/03/2022 todo va con OAuth.

Configuración

Si hemos seguido esta guía ya tendremos la configuración hecha: Configuración de proyectos Spring Boot para FundeWebJs. Debemos repasar que no nos falte ninguna dependencia o repository en el pom, las clases de configuración, que tengamos las properties que se indican, y que la clase inicial tenga la anotación correspondiente (que básicamente es como decir que se revise todo, pero es importante subrayarlo).

Token en métodos REST

Una vez configurado, las clases de nuestros servicios REST deben llevar esta anotación:

@RequestMapping( "${app.server.path}" )

Y para obtener el token en los métodos REST, utilizamos @AuthenticationPrincipal:

@GetMapping( PRIVATE_PREFIX + API_VERSION + "/resguardos" )
public ResponseEntity getResguardos( @AuthenticationPrincipal Jwt jwt ) { ... }

Como vemos, tendremos tanto esto como el header. Cuando entremos con token POSE, jwt será null. Podemos obtener el identificador del token simplemente con:

jwt.getSubject()

Obtener datos del usuario a partir del token

Para esto, Recursos Humanos tiene un servicio al que podremos llamar a través de una librería: fundewebjs-serviciosrrhh-client. Esto ya lo tendremos configurado si hemos hecho correctamente el primer paso.

Para utilizarla, simplemente la incluimos con @Autowired:

	@Autowired
	private ServiciosRrhhClientService serviciosRrhhClient;

Y llamamos al método getAfiliacion pasándole el subject del token y el Jwt (cogido con @AuthenticationPrincipal): 

// Versión sólo OAuth
serviciosRrhhClient.getAfiliacion(subject, jwt);

Esto nos devuelve un objeto de tipo AfiliacionDTO, que contiene información del usuario. En concreto, se compone de estos campos:

private String identificador;
private String letra;
private String tipoIdentificador;
private String nombre;
private String apellido1;
private String apellido2;
private Date fechaNacimiento;
private String sexo;
private String nacionalidad;
private DireccionDTO direccion;
private List<String> telefonos;
private List<String> emails;
private List<CentroAlumnoDTO> centros;

Todos tienen sus correspondientes getters. Lo más normal es que utilicemos el identificador, pero los demás también pueden ser útiles. En el caso de los emails, si un usuario tiene varios, el primero de la lista es el principal.

Forma antigua

Dejamos temporalmente la documentación antigua, con el token POSE, por si hace falta para rescatar algún proyecto antiguo:

Login con token POSE

En nuestro backend, cuando usemos el login a través del Portal de Servicios, sólo necesitaremos poder comprobar el token. Para ello, necesitaremos incluir el secret en nuestro proyecto. En local, debemos incluir el secret como una variable de entorno en Eclipse. Esto lo haremos yendo a Run As → Run Configurations..., tendremos que seleccionar el proyecto correspondiente en la barra de la izquierda, e irnos a la pestaña Environment. Ahí le damos a Add.., e introducimos la variable ES_UM_JWT_SECRET, y el correspondiente valor del secret:


Pulsamos en Apply, y podemos cerrar la ventana.

Después, incluiremos la variable es.um.jwt.secret en el archivo application.properties de nuestro proyecto, así:

es.um.jwt.secret=${ES_UM_JWT_SECRET}

El valor del secret variará en cada entorno, pero para poder probarlo en local, tendremos que incluir el secret del entorno correspondiente con el que estemos haciendo las pruebas. Al hacerlo de esta manera, evitamos que el secret quede expuesto en GitLab.

Para desarrollar en local, utilizaremos el token de micampusdesa, así que hará falta incluir el secret de desarrollo. Para obtener este secret, preguntad a MNCS, pues por seguridad no lo compartimos aquí.

Una vez hecho esto, ya solo nos queda comprobar el token en los métodos privados de nuestro servicio REST, que está explicado en el apartado Cómo comprobar el token.

Cómo comprobar el token

Recibiremos el token por parte del front-end como un String, en el header Authorization, que tendrá la siguiente forma:

# Bearer token
"Bearer tokenCifrado"

Para realizar la comprobaciones en cada petición que llegue, podemos utilizar filtros que añadiremos a la configuración de seguridad de nuestra aplicación.

Filtro de autorización

Para comprobar el token en cada petición que se haga, tendremos un filtro de autorización. Un ejemplo, podría ser este:

@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

	@Autowired
	private JwtTokenUtil jwtTokenUtil;

	// RUTAS A NO FILTRAR: INCLUIR RUTAS PÚBLICAS Y RUTAS DE OAUTH, PUES SE FILTRAN DIRECTAMENTE
	private List<String> skipUrls = Arrays.asList( "/public/**", "/mncs/consulta-expedientes-api/**" );
	private AntPathMatcher pathMatcher = new AntPathMatcher();

	@Override
	protected boolean shouldNotFilter( HttpServletRequest request ) throws ServletException {
		return skipUrls.stream().anyMatch( p -> pathMatcher.match( p, request.getServletPath() ) );
	}

	@Override
	protected void doFilterInternal( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
			FilterChain filterChain ) throws ServletException, IOException {

		final String token = httpServletRequest.getHeader( HEADER_AUTHORIZATION_KEY );

  		// INCLUIR AQUÍ LAS COMPROBACIONES NECESARIAS DE LA APLICACIÓN
		// Con getClaim sólo se comprueba si el token es correcto
		try {
			jwtTokenUtil.getClaim( token );
		} catch ( ServicioException e ) {
			httpServletResponse.setStatus( e.getStatus().value() );
			return;
		}

		// OBLIGATORIO
		filterChain.doFilter( httpServletRequest, httpServletResponse );
	}
}

Este filtro lo utilizaremos como mínimo para comprobar el token POSE. Marcando la clase con @Component y extendiendo a OncePerRequestFilter, directamente se ejecutará en cada petición que se reciba.

Se deben ignorar rutas públicas y rutas con token OAuth, pues se filtran directamente, y si intentamos abrir un token OAuth con los métodos de token POSE saltará un error. Para saltarse estas rutas, tenemos en el código el array skipUrls. Aquí, tenemos que añadir la ruta base OAuth a mano, que en el ejemplo es /mncs/consulta-expedientes-api/**. Después, teniendo eso, el método shouldNotFilter se encarga de hacer match de la ruta a la que se ha llamado.

Después, si se ejecuta el filtro, se ejecuta el código que indiquemos en doFilterInternal. Aquí, como comentamos, por lo menos comprobaremos el token POSE llamando a jwtTokenUtil.getClaim, pero si por la lógica de la aplicación se necesita filtrar algo más, también se añadirá aquí.

Constantes

En las siguientes explicaciones se utilizan algunas constantes. Para definirlas, podemos tener una clases Constants.java, en el paquete base del proyecto, por ejemplo. Esta clase, únicamente con las constantes que se utilizan en esta explicación y las cadenas para los métodos REST públicos y privados, sería la siguiente:

package es.um.atica.-----;


public final class Constants {

	private Constants() {}

	public static final String PUBLIC_PREFIX = "/public";
	public static final String PRIVATE_PREFIX = "/private";
	public static final String AUTHORIZATION_HEADER = "Authorization";
	public static final String TOKEN_BEARER_PREFIX = "Bearer";

}


Obtener datos del token

Para obtener los datos del token, podemos obtener sus Claims, que contienen la información que se pasa en el token. Se utiliza el secret para descifrarlo. Para esto tenemos una clase JwtTokenUtil que contiene el método getClaim, que devuelve un objeto Claims o lanza una excepción si el token esta vacío, es incorrecto o ha expirado:

@Component
public class JwtTokenUtil {

	private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager
			.getLogger( JwtTokenUtil.class );

	@Value( "${es.um.jwt.secret}" )
	private String secret;

	/**
	 * Obtiene los claims del token. Si el token ha expirado, lanza una TokenExpiredException. Si el token no es válido,
	 * lanza una UnauthorizedException.
	 *
	 * @param token
	 * @return claims
	 */
	public Claims getClaim( String token ) {
		// Comprobamos que el token tiene contenido y empieza por "Bearer "
		if ( ( token == null ) || token.isEmpty() || !token.startsWith( TOKEN_BEARER_PREFIX ) ) {
			throw new UnauthorizedException();
		}
		token = token.replace( TOKEN_BEARER_PREFIX + " ", "" ); // Quitamos "Bearer" y el espacio en blanco

		try {
			// Obtenemos claims
			final Claims claims = Jwts.parser().setSigningKey( TextCodec.BASE64.encode( secret ) )
					.parseClaimsJws( token ).getBody();
			log.debug( "El claims es: " + claims.toString() );
			log.debug( "El subject es: " + claims.getSubject() );

			// Comprobamos si el token está expirado
			final Date expiration = claims.getExpiration();
			if ( Instant.now().isAfter( expiration.toInstant() ) ) {
				log.error( "Expiration Date: " + expiration.toString() );
				log.error( "Current Instant: " + Instant.now().toString() );
				log.error( "Error el token " + token + " está caducado" );
				throw new TokenExpiredException();
			}

			return claims;

		} catch ( final ExpiredJwtException e ) {
			log.error( "Error el token está caducado" );
			throw new TokenExpiredException();
		} catch ( final JwtException e ) {
			log.error( "Error filtering:" + e );
			throw new UnauthorizedException();
		}

	}
}

Como vemos, tiene que ir anotada con @Component, y después la instanciaremos en nuestros servicios o filtros con @Autowired. También vemos que cogemos el valor del secret y de cuándo expira de nuestro application.properties.

El secret lo utilizamos para decodificar el token, y obtenemos un objeto de tipo Claims. Podemos ver la información que contiene con toString(), pero lo más importante es obtener el email del usuario logeado, que lo haremos con .getSubject().

Si el token no es válido, o si ha expirado, se lanza una excepción. Para ello nos hemos definido excepciones propias, en un paquete exceptions.

Con esto, un ejemplo de cómo obtener el email del usuario teniendo el token, sería así:

@Autowired
private JwtTokenUtil jwtTokenUtil;

...

Claims claims = jwtTokenUtil.getClaim(token);
String email = claims.getSubject();

Token en métodos REST

Con lo visto anteriormente, en las llamadas a nuestros métodos REST privados se comprobará automáticamente el token. Si queremos utilizarlo para la lógica que se haga en dicho método, debemos incluir como parámetro de la función el header Authorization (constante AUTHORIZATION_HEADER) como requerido, con @RequestHeader:

	@GetMapping( PRIVATE_PREFIX + "/titulaciones" )
	public ResponseEntity getTitulaciones(
			@RequestHeader( value = AUTHORIZATION_HEADER, required = true ) String token ) {

		RestExpedientes.log.info( " ############################################### titulaciones: " );

		return new ResponseEntity<>( expedientesService.getTitulaciones( token ), HttpStatus.OK );
	}

Excepciones

Utilizamos las siguientes excepciones, que también se incluyen en el cliente CAS:

ServicioException.java

/**
 * ServicioNotFoundException
 */
public class ServicioException extends RuntimeException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.BAD_REQUEST;
 
	protected ServicioException( String msg ) {
		super( msg );
	}
 
	public HttpStatus getStatus() {
		return ServicioException.status;
	}
 
}

TokenExpiredException.java

/**
 * ServicioNotFoundException
 */
public class TokenExpiredException extends ServicioException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.UNAUTHORIZED;
 
	public TokenExpiredException() {
		super( "Token_expired" );
	}
 
	@Override
	public HttpStatus getStatus() {
		return TokenExpiredException.status;
	}
 
}

UnauthorizedException.java

/**
 * ServicioNotFoundException
 */
public class UnauthorizedException extends ServicioException {
 
	private static final long serialVersionUID = 1L;
 
	private static final HttpStatus status = HttpStatus.UNAUTHORIZED;
 
	public UnauthorizedException() {
		super( "No tiene acceso para solicitar este servicio " );
	}
 
	@Override
	public HttpStatus getStatus() {
		return UnauthorizedException.status;
	}
 
}

Login con token OAuth

En primer lugar, debemos tener configurada nuestra aplicación para que soporte OAuth, lo haremos siguiendo esta guía: Migración del backend a soporte oAuth

Una vez hecho esto, para obtener el token en nuestros servicios REST, utilizamos @AuthenticationPrincipal:

@GetMapping( PRIVATE_PREFIX + API_VERSION + "/resguardos" )
public ResponseEntity getResguardos( @AuthenticationPrincipal Jwt jwt,
	@RequestHeader( value = "Authorization", required = true ) String token ) { ... }

Como vemos, tendremos tanto esto como el header. Cuando entremos con token POSE, jwt será null. Podemos obtener el identificador del token simplemente con:

jwt.getSubject()

Con la configuración que habremos añadido, se tienen dos endpoints:

@RequestMapping( "/", "${app.server.path}" )

Esto es, la ruta base del proyecto para token POSE, y la ruta base + "/grupo/proyecto" para OAuth.

Aunque tengamos estos endpoints diferentes, los métodos no están duplicados, pero no será necesario distinguir qué tipo de token recibimos. Cada tipo de token puede llevar un identificador diferente, pero hemos hecho una librería que hace de cliente del servicio de recursos humanos para obtener el identificador de cada usuario, lo vemos en el siguiente apartado.