Skip to content

Sécurité

mekor-dev edited this page Sep 30, 2020 · 11 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é, d'encrypter 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

  • 400 : Header mal formé
  • 401 : Erreur d'authentification

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

Annotations par défaut

Les annotations suivantes sont déjà présentes, et pourront être utilisées dans la plupart des projets :

  • @ControlPublic : Accessible à tous
  • @ControlLoggedIn : Accessible aux utilisateurs authentifiés
  • @ControlAdmin : Accessible aux admins
  • @ControlAccessNotification : Vérifie si l'utilisateur authentifié a accès a la notification en pathparam

Le fonctionnement de ces annotations est détaillé dans les chapitres suivants.

Sans paramètre

Par défaut, afin d'éviter les oublis, l'application renvoie une erreur 500 sur toutes les méthodes non protégées de l'API. 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

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