Skip to content

Security

mekor-dev edited this page Sep 29, 2020 · 9 revisions

Authentification

JWT

L'authentification sur l'API se fait à l'aide de JSON Web Tokens. Un service de l'API permettra d'échanger un couple username / password contre deux token :

  • accessToken : Permet d'accéder aux service requérant d'être authentifié.
  • refreshToken: Permet de récupérer un nouvel accessToken dans le cas ou le notre est expiré.

Plus d'information sur JWT : https://jwt.io/

Librairie utilisée pour créer / parser les JWT : https://github.com/jwtk/jjwt

Récupération des tokens

Requête

Le service suivant permet de récupérer les tokens :

http://host/api/users/authenticate (AccessTokenAPI.authenticate())

La requête devra contenir un header Authorization contenant le couple username / password après les transformations suivantes :

  • concaténation des deux chaines séparé par un ":"
      fred.allen@mail.com:testPassword
  • Encodage en base 64
      ZnJlZC5hbGxlbkBtYWlsLmNvbTp0ZXN0UGFzc3dvcmQ=
  • Préfixage par 'Basic '
      Basic ZnJlZC5hbGxlbkBtYWlsLmNvbTp0ZXN0UGFzc3dvcmQ=

Il est recommandé, pour plus de sécurité, de chiffre ce header, surtout si vous n'êtes pas en HTTPS.

Réponse

Ce service renverra un objet contenant l'accessToken, le refreshToken et l'utilisateur qui a été authentifié.

{
  "accessToken" : "...",
  "refreshToken" : "...",
  "user" : {"..."}
}
  • 400 : Le header une fois décodé n'est pas au format username:password
  • 401 : L'authentification a échoué. Le header n'est pas présent, ou mal préfixé, ou le couple username / password est invalide.

Joindre l'accessToken aux requêtes.

Un fois l'utilisateur authentifié, il faudra joindre son accessToken dans toutes les prochaines requêtes qu'il fera. Pour cela, il suffit d'ajouter son token dans le header Authorization, prefixé de 'Bearer '

ex : Authorization=Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmcmVkLmFsbGVuQG1haWwuY29tIiwiaWF0IjoxNTQ4OTQ5OTQ1LCJleHAiOjE1NDg5NTE3NDV9.I_TzLTGqnp0S0JAccBR9xmjZvyRbcHPZ-01S_7-H-SA

En cas d'erreur, le code 400 (header mal formé) ou 401 (erreur d'authentification) sera renvoyé.
Le processus de vérification est fait par la classe AccessTokenFilter

Un nouvel accessToken est également retourné dans chaque réponse de l'API en header, si l'utilisateur est déjà authentifié, grâce à la classe AccessTokenRefreshFilter

Rafraîchir le token

Requête

Le service suivant permet de récupérer un nouvel accessToken à partir du refreshToken :

http://host/api/users/refresh (AccessTokenAPI.refreshAccessToken())

Il faudra, en body de la requête passer le refreshToken.

Réponse

Ce service renverra un objet contenant l'accessToken et l'utilisateur qui a été authentifié.

{
  "accessToken" : "...",
  "user" : {"..."}
}

Vérification des droits

Sans paramètre

Par défaut, l'application refuse l'accès à toutes les méthodes de l'API qui ne sont pas protégées. Pour protéger une méthode, on peut l'annoter avec une annotation que l'on aura créé, et vérifier les droits dans ApiControlFilter.

//Création de l'annotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ControlAdmin {
}
//Annotation de la méthode à protéger
@GET
@ControlAdmin
public void adminOnly(){
}
//ApiControlFilter
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
	if (apiUtils.isAnnotationPresent(resourceInfo, ControlAdmin.class)) {
		authorization.isAdmin(); // throw AuthorizationException si l'utilisateur n'est pas admin
	}
}

Avec paramètre

Avec la méthode précédente, on ne peut pas ajouter de paramètre. Par exemple comment vérifier si l'utilisateur authentifié a le droit d'accéder à la notification demandée lors de l'appel /api/notifications/{id}

Avec un peu d'astuce, (et malheureusement pas mal de bidouille) on peut récupérer l'ID de la notification passée en PathParam dans la classe APIControlFilter.

On commence comme dans le châpitre précédent :

//Création de l'annotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ControlAccessNotification {
}
//Annotation de la méthode à protéger
@GET
@Path("/notifications/{id}")
@ControlAccessNotification
public Response put(@PathParam("id") Long id) {
	//process
}

Il faut désormais ajouter une entrée dans l'enum ControlParams

//Ajout du type de paramètre
public enum ControlParams {
	NOTIFICATION_ID;
}

On peut alors indiquer que l'un de nos PathParam est également un ControlParam du type choisi grâce à l'annotation @ControlParam. Attention, seul les PathParam fonctionneront.

@GET
@Path("/notifications/{id}")
@ControlAccessNotification
public Response put(@ControlParam(ControlParams.NOTIFICATION_ID) @PathParam("id") Long id) {
	//process
}

On peut désormais récupérer notre ID de notification dans `APIControlFilter`

```java
public void filter(ContainerRequestContext requestContext) throws IOException {
	if (apiUtils.isAnnotationPresent(resourceInfo, ControlAccessNotification.class)) {
		Long notifID = findControlParamValue(ControlParams.NOTIFICATION_ID, Long.class); // Récupération de l'ID de la notification
		authorization.canAccessNotification(notifService.findByIDHandleNotFound(notifID, currentRequest.isAdmin()));
	}
}

Comme vous l'avez peut-être remarqué, la méthode magique, c'est findControlParamValue(). Elle va chercher le PathParam qui est également annoté avec ControlParam et le bon type de ControlParam. Si vous êtes curieux, la voici :

private <T> T findControlParamValue(ControlParams controlParam, Class<T> type) {
	try {
		if (type.isAssignableFrom(Long.class)) {
			for (Parameter param : resourceInfo.getResourceMethod().getParameters()) {
				ControlParam annotation = param.getAnnotation(ControlParam.class);
				if (annotation != null) {
					if (controlParam == annotation.value()) {
						return type.cast(Long.parseLong(uriInfo.getPathParameters().getFirst(param.getAnnotation(PathParam.class).value())));
					}
				}
			}
		}
	}
	catch (Exception e) {
		log.error("Could not find pathParam value for controlParam {} and type {}", controlParam, type, e);
		throw new AuthorizationException(500, "Error while authorizing access to the service");
	}
	log.error("Could not find pathParam value for controlParam {} and type {}", controlParam, type);
	throw new AuthorizationException(500, "Error while authorizing access to the service");
	}

Clone this wiki locally