-
Notifications
You must be signed in to change notification settings - Fork 0
Sécurité
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
accessTokendans 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
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.
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.
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
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.
Ce service renverra un objet contenant l'accessToken et l'utilisateur qui a été authentifié.
{
"accessToken" : "...",
"user" : {"..."}
}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.
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
}
}Pour ajouter un paramètre, il suffit d'ajouter le nom du pathParam que l'on veut passer a l'APIControlFilter en tant que paramètre de l'annotation
On crée d'abord notre annotation, contenant un ou plusieurs paramètres :
//Création de l'annotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ControlAccessNotification {
public String notifID();
}On décore ensuite la méthode à protéger, en pensant à bien ajouter le nom du pathparam :
//Annotation de la méthode à protéger
@GET
@Path("/notifications/{id}")
@ControlAccessNotification(notifID = "id")
public Response put(@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 {
log.debug("Entering filter()");
// ControlAccessNotification
if (apiUtils.isAnnotationPresent(resourceInfo, ControlAccessNotification.class)) {
ControlAccessNotification annotation = apiUtils.getAnnotation(resourceInfo, ControlAccessNotification.class); // récupération de l'annotation
Long notifID = Long.parseLong(findPathParamValue(annotation.notifID())); // récupération de la valeur du pathparam
log.debug("ControlAccessNotification for notifID : {}", notifID);
authorization.canAccessNotification(notifService.findByIDHandleNotFound(notifID, currentRequest.isAdmin()));
}
// No annotation
else {
log.error("No control annotation found on the service method");
throw new AuthorizationException(500, "Error while authorizing access to the service");
}
log.debug("Leaving filter()");
}Comme vous l'avez sûrement remarqué, la méthode magique, c'est findPathParamValue(). Elle va chercher le PathParam qui porte le nom demandé :
private String findPathParamValue(String pathparam) {
for (Entry<String, List<String>> param : uriInfo.getPathParameters().entrySet()) {
if (param.getKey().equals(pathparam)) {
return param.getValue().get(0);
}
}
log.error("Could not find pathParam with given name for notifID param");
throw new AuthorizationException(500, "Error while authorizing access to the service");
}