-
Notifications
You must be signed in to change notification settings - Fork 0
Security
We use JSON WEb Tokens to authenticate users on the platform. An endpoint consumes a username / password couple and produces two tokens :
- accessToken : Allow access to protected endpoints.
-
refreshToken: Allow to fetch a new
accessTokenif it has expired.
Pore information about JWT : https://jwt.io/
Lib used to build / parse JWT : https://github.com/jwtk/jjwt
The following endpoints produces the tokens:
http://host/api/users/authenticate (AccessTokenAPI.authenticate())
The request must contain a Authorization header with the the username / password couple modified as followed:
- concatenation of the two strings, separated by ":"
fred.allen@mail.com:testPassword
- Base 64 enconding
ZnJlZC5hbGxlbkBtYWlsLmNvbTp0ZXN0UGFzc3dvcmQ=
- Prefixing by 'Basic '
Basic ZnJlZC5hbGxlbkBtYWlsLmNvbTp0ZXN0UGFzc3dvcmQ=
For more security, it is wise to encrypt this header. Even more if you are not using HTTPS.
This endpoint returns a object containing the accessToken, the refreshToken, and authenticated user.
{
"accessToken" : "...",
"refreshToken" : "...",
"user" : {"..."}
}- 400 : The user is does not follow the correct format
- 401 : Authentication failed. The header is missing, has a bad prefix or the username / password couple is invalid.
When the user is authenticated, you must join their accessToken in every following requests.
You juste need to add his token in the Authorization header, prefixed by 'Bearer '
ex : Authorization=Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmcmVkLmFsbGVuQG1haWwuY29tIiwiaWF0IjoxNTQ4OTQ5OTQ1LCJleHAiOjE1NDg5NTE3NDV9.I_TzLTGqnp0S0JAccBR9xmjZvyRbcHPZ-01S_7-H-SA
- 400: The header is not correctly formated
- 401: Authentication failed
The verification process is done in the AccessTokenFilter class.
If the user is authenticate itself when sending a request, a new accessToken will also be returned in the header of the response, thanks to the AccessTokenRefreshFilter class.
The following endpoint returns consumes a refreshToken and produces a new accessToken:
http://host/api/users/refresh (AccessTokenAPI.refreshAccessToken())
You need to pass the refreshToken in the request body.
This endpoint returns an object containing the accessToken and the authenticated user.
{
"accessToken" : "...",
"user" : {"..."}
}Following annotations are already present and can be used in any project:
- @ControlPublic: Anyone can access.
- @ControlLoggedIn: Authenticated users can access.
- @ControlAdmin: Admins can access
- @ControlAccessNotification: Check if the authenticated user can access the notification given in pathparam
The usage of these annotations is explained in the next chapters.
To avoid any omission, the application returns an error 500 for every non protected endpoint.
To protect an endpoint, you can annotate it with a annotation you created and check the access rights in ApiControlFilter.
//Annotation creation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ControlAdmin {
}//Annotation of the endpoint
@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
}
}With the previous method, you cannot add a parameter. For exemple, you cannot check if the authenticated user can access a notification for the /api/notifications/{id} endpoint.
With a little trick, we can get the ID of the notification that we passed in the PathParam in the ApiControlFilter class.
We start as in the previous chapter:
//Annotation creation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ControlAccessNotification {
}//Annotation of the endpoint
@GET
@Path("/notifications/{id}")
@ControlAccessNotification
public Response put(@PathParam("id") Long id) {
//process
}We must now create an entry in the ControlParams enum:
//Adding the Controlparams type
public enum ControlParams {
NOTIFICATION_ID;
}You can now annotate a parameter which is already annotated with @Pathparm, to indicate that this is also a ControlParam.
@GET
@Path("/notifications/{id}")
@ControlAccessNotification
public Response put(@ControlParam(ControlParams.NOTIFICATION_ID) @PathParam("id") Long id) {
//process
}Now we can get the ID of the notification in ApiControlFilter
public void filter(ContainerRequestContext requestContext) throws IOException {
if (apiUtils.isAnnotationPresent(resourceInfo, ControlAccessNotification.class)) {
Long notifID = Long.parseLong(findControlParamValue(ControlParams.NOTIFICATION_ID)); // Récupération de l'ID de la notification
authorization.canAccessNotification(notifService.findByIDHandleNotFound(notifID, currentRequest.isAdmin()));
}
}The magic happens in the findControlParamValue method. Here it is:
private String findControlParamValue(ControlParams controlParam) {
try {
for (Parameter param : resourceInfo.getResourceMethod().getParameters()) {
ControlParam annotation = param.getAnnotation(ControlParam.class);
if (annotation != null) {
if (controlParam == annotation.value()) {
return uriInfo.getPathParameters().getFirst(param.getAnnotation(PathParam.class).value());
}
}
}
}
catch (Exception e) {
log.error("Could not find pathParam value for controlParam {}", controlParam, e);
throw new AuthorizationException(500, "Error while authorizing access to the service");
}
log.error("Could not find pathParam value for controlParam {}", controlParam);
throw new AuthorizationException(500, "Error while authorizing access to the service");
}