Skip to content

Security

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

Authentication

JWT

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 accessToken if it has expired.

Pore information about JWT : https://jwt.io/

Lib used to build / parse JWT : https://github.com/jwtk/jjwt

Getting the tokens

Request

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.

Response

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.

Join the accessToken to requests

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.

Refresh the token

Requête

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.

Reponse

This endpoint returns an object containing the accessToken and the authenticated user.

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

Rights check

Default annotations

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.

Without parameter

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 parameter

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");
}

Clone this wiki locally