Explain Codes LogoExplain Codes Logo

How to implement REST token-based authentication with JAX-RS and Jersey

java
jwt
authentication
authorization
Alex KataevbyAlex Kataev·Sep 8, 2024
TLDR

When using JAX-RS and Jersey, one way to secure your REST API with token-based authentication is by making use of a ContainerRequestFilter. This allows you to intercept the request processing, extract the token, and validate it:

@Provider public class AuthFilter implements ContainerRequestFilter { // All tokens are awesome but some are just more awesome than others @Override public void filter(ContainerRequestContext ctx) { String token = ctx.getHeaderString(HttpHeaders.AUTHORIZATION); // If token makes us happy, we forge ahead if (token != null && tokenValid(token)) { ctx.setSecurityContext(new SecurityContext() { @Override public Principal getUserPrincipal() { return () -> getUser(token); } @Override public boolean isUserInRole(String role) { return roleValid(role, token); } // Are you worthy? @Override public boolean isSecure() { return ctx.getSecurityContext().isSecure(); } @Override public String getAuthenticationScheme() { return "Bearer"; } }); } else { // If token is sad, we say goodbye ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build()); } } // Validate token, aka checking if it's chocolate private boolean tokenValid(String token) { // Replace this validation logic with your brand of enthusiasm return true; } // Decode user from token, feels like peekaboo private String getUser(String token) { // Replace with real user extraction, always respect privacy! return "username"; } // Check if the role fulfills its destiny private boolean roleValid(String role, String token) { // Replace with actual permission slip return true; } }

Essentially, you are creating a custom filter for authentication, extracting the token from headers, validating it and binding a user to the context. If the token isn't valid, you stop the request with an UNAUTHORIZED response. This involves implementing the validation and extraction logic based on how your token is structured.

Core aspects for optimal token-based authentication

Minding your tokens

Use the JWTs (JSON Web Tokens) which are compact and provide a secure method of transmitting information between parties. To implement this, create an authentication endpoint:

@Path("/auth") public class AuthenticationEndpoint { // Everyone likes a good handshake @POST @Produces(MediaType.APPLICATION_JSON) public Response authenticateUser(Credentials credentials) { try { String token = validateUser(credentials); // If everything checks out, we say hello with a token return Response.ok(new TokenResponse(token)).build(); } catch (AuthenticationException e) { // If things go south, we say it couldn't be done return Response.status(Response.Status.UNAUTHORIZED).entity("User cannot be authenticated").build(); } } private String validateUser(Credentials credentials) { // Validate user credentials // Issue the golden ticket (token) if we have a winner } }

Don't neglect token claims like expiry time, issuer, and a unique identifier (jti). This will reduce the risk and help with token revocation when necessary.

Token lifecycle management

When a user changes their password or logs out, it's crucial to invalidate all associated tokens. One strategy is to store just the identifiers of JWTs (not the full tokens) to manage efficient token revocation, especially in a distributed system.

Advanced considerations for roles and authorization handling

Create a custom annotation e.g., @Secured to label endpoints needing security and introduce an AuthorizationFilter that uses ResourceInfo to apply the right authorizations:

@Secured @Provider public class AuthorizationFilter implements ContainerRequestFilter { // Who, what, where, when - The Info Quartet @Context private ResourceInfo resourceInfo; @Override public void filter(ContainerRequestContext requestContext) { // Call the shots on authorizations, that's right, you're the boss } }

Use method-specific annotations like @RolesAllowed for role-based access. Be sure to check the token for the required roles before moving onto the method execution.

Wide-reaching user data with Dependency Injection

CDI (Context and Dependency Injection) can be your friend here to inject authenticated user data across your application, offering a streamlined experience for handling user-specific data:

@Inject private AuthenticatedUserProvider authProvider; // authProvider is like your backstage pass - use it wisely!