Versiones comparadas

Clave

  • Se ha añadido esta línea.
  • Se ha eliminado esta línea.
  • El formato se ha cambiado.

Tabla de contenidos

Actualmente, en el Portal de Servicios se utiliza el token POSE, pero se está migrando para utilizar token OAuth. Por lo tanto, de momento tenemos que incluir ambos; nuestra aplicación tendrá login dual, aceptará tanto token POSE como token OAuth.

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:

Image Removed

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í:

Bloque de código
languagexml
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:

Bloque de código
languagejava
themeEclipse
# 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:

Bloque de código
languagejava
themeEclipse
titleFiltro de autorización
linenumberstrue
@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:

Bloque de código
languagejava
linenumberstrue
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:

Bloque de código
languagejava
themeEclipse
titleJwtTokenUtil
linenumberstrue
@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í:

Bloque de código
@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:

Bloque de código
languagejava
themeEclipse
titleCómo comprobar el token
linenumberstrue
	@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

Bloque de código
languagejava
themeEclipse
titleServicioException
linenumberstrue
/**
 * 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

Bloque de código
languagejava
themeEclipse
titleTokenExpiredException
linenumberstrue
/**
 * 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

Bloque de código
languagejava
themeEclipse
titleUnauthorizedException
linenumberstrue
/**
 * 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

...

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:

Bloque de código
languagejava
@RequestMapping( "${app.server.path}" )

Y para obtener el token en los métodos REST, utilizamos Una vez hecho esto, para obtener el token en nuestros servicios REST, utilizamos @AuthenticationPrincipal:

Bloque de código
languagejava
@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:

Bloque de código
languagejava
jwt.getSubject()

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

Bloque de código
languagejava
@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.

Librería de cliente RRHH para obtener datos de usuario

En la mayoría de aplicaciones querremos obtener el identificador del usuario logeado para pedir datos a base de datos. Para obtener este identificador, rrhh dispone de un servicio, y tenemos una librería que hace de cliente de éste, facilitando la llamada sin tener que diferenciar entre el tipo de token que tenemos y permitiendo cachear las respuestas.

Para incluir la librería, en el pom.xml, en el bloque de dependencies, añadiremos esta:

Bloque de código
languagexml
		<!-- Servicios RRHH Client -->
		<dependency>
			<groupId>es.um.atica.fundewebjs.fundewebjs-api</groupId>
			<artifactId>fundewebjs-serviciosrrhh-client</artifactId>
			<version>1.0.2-SNAPSHOT</version>
		</dependency>

Y en el bloque de repositories, si no lo tenemos, este:

Bloque de código
languagexml
		<repository>
			<id>fundewebjs.archiva.atica.umu.es</id>
			<name>ATICA - UMU Repository - FundeWebJS</name>
			<url>https://archiva.um.es/archiva/repository/FundeWebJS/</url>
			<releases>
				<enabled>true</enabled>
			</releases>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>

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 Después, para utilizarla, simplemente la incluimos con @Autowired:

...

Y llamamos al método getAfiliacion pasándole el subject del tokenel token como String (cogido con @RequestHeader en nuestro método REST) y  y el Jwt (cogido con @AuthenticationPrincipal)

Bloque de código
languagejava
// Versión sólo OAuth
serviciosRrhhClient.getAfiliacion(subject, jwt);

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

Bloque de código
languagejava
	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 String telefonoNotificaciones;
private String emailNotificaciones;
	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.

Se recomienda ver también la página Uso de los entornos de micampus en local, sobre la configuración de propiedades para trabajar con los entornos en local.