Skip to content

Security

mekor-dev edited this page Nov 20, 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 parameters

To add a parameter, we just need to add the name of the pathparam we want to use in the APIControlFilter as the annotation parameter.

First, we crate our annotation, with one or many parameters:

//Annotation creation
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ControlAccessNotification {

   public String notifID();

}

Then, we decorate the method we want to protect and add the pathparam name:

//Annotation of the endpoint
@GET
@Path("/notifications/{id}")
@ControlAccessNotification(notifID = "id")
public Response put(@PathParam("id") Long id) {
	//process
}

We can now get our notification ID in the 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); // getting the annotation
		Long notifID = Long.parseLong(findPathParamValue(annotation.notifID())); // getting the pathparam value
		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()");
}

As you may have noticed, the magic method is findPathParamValue(). It finds the pathparam with the given name:

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

Clone this wiki locally