Secure Your Jersey REST APIs – Complete Authentication & Authorization Guide

Exposing a REST endpoint is only half the job — you also need to control who can call it and what they are allowed to do. Jersey, the reference implementation of Jakarta RESTful Web Services (JAX-RS), provides a clean extension point through ContainerRequestFilter that lets you intercept every request before it reaches your resource class. In this guide you will implement HTTP Basic authentication, JWT Bearer-token validation, and role-based authorization step by step, using plain Jersey and then the Spring Boot integration.

Dependencies

<!-- Jersey (Jakarta EE 10) -->
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>3.1.5</version>
</dependency>

<!-- JWT (Auth0) -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>

Part 1 — HTTP Basic Authentication Filter

HTTP Basic sends credentials as Base64(username:password) in the Authorization header. The filter below validates credentials and injects a SecurityContext so downstream resources can call isUserInRole().

package com.ankurm.security;

import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;

import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.Base64;
import java.util.Map;
import java.util.Set;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class BasicAuthFilter implements ContainerRequestFilter {

    // In a real app load this from a database or LDAP
    private static final Map<String, String> USERS = Map.of(
        "alice", "s3cr3t",
        "admin", "adminPass"
    );
    private static final Map<String, Set<String>> ROLES = Map.of(
        "alice", Set.of("USER"),
        "admin", Set.of("USER", "ADMIN")
    );

    @Override
    public void filter(ContainerRequestContext ctx) {
        String authHeader = ctx.getHeaderString("Authorization");

        if (authHeader == null || !authHeader.startsWith("Basic ")) {
            abort(ctx); return;
        }

        // Decode Base64 credentials
        byte[] decoded = Base64.getDecoder().decode(authHeader.substring(6));
        String[] parts  = new String(decoded, StandardCharsets.UTF_8).split(":", 2);
        if (parts.length != 2) { abort(ctx); return; }

        String username = parts[0];
        String password = parts[1];

        if (!password.equals(USERS.get(username))) {
            abort(ctx); return;
        }

        // Inject SecurityContext so @RolesAllowed works
        Set<String> userRoles = ROLES.getOrDefault(username, Set.of());
        ctx.setSecurityContext(new SecurityContext() {
            @Override public Principal getUserPrincipal() { return () -> username; }
            @Override public boolean isUserInRole(String role) { return userRoles.contains(role); }
            @Override public boolean isSecure() { return ctx.getSecurityContext().isSecure(); }
            @Override public String getAuthenticationScheme() { return "BASIC"; }
        });
    }

    private void abort(ContainerRequestContext ctx) {
        ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED)
            .header("WWW-Authenticate", "Basic realm="MyAPI"")
            .build());
    }
}

Part 2 — JWT Bearer-Token Filter

For stateless APIs, JWT is the modern standard. The filter verifies the token signature and expiry, then injects the username and roles from the claims.

package com.ankurm.security;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.*;
import jakarta.ws.rs.core.*;
import jakarta.ws.rs.ext.Provider;

import java.security.Principal;
import java.util.List;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class JwtAuthFilter implements ContainerRequestFilter {

    private static final String SECRET = "change-me-in-prod-use-env-var";
    private static final Algorithm ALGORITHM = Algorithm.HMAC256(SECRET);

    @Override
    public void filter(ContainerRequestContext ctx) {
        String authHeader = ctx.getHeaderString(HttpHeaders.AUTHORIZATION);

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            abort(ctx, "Missing or malformed Authorization header"); return;
        }

        String token = authHeader.substring(7);
        DecodedJWT jwt;
        try {
            jwt = JWT.require(ALGORITHM).build().verify(token);
        } catch (JWTVerificationException e) {
            abort(ctx, "Invalid or expired token"); return;
        }

        String username = jwt.getSubject();
        List<String> roles = jwt.getClaim("roles").asList(String.class);

        ctx.setSecurityContext(new SecurityContext() {
            @Override public Principal getUserPrincipal() { return () -> username; }
            @Override public boolean isUserInRole(String r) { return roles != null && roles.contains(r); }
            @Override public boolean isSecure() { return ctx.getSecurityContext().isSecure(); }
            @Override public String getAuthenticationScheme() { return "BEARER"; }
        });
    }

    private void abort(ContainerRequestContext ctx, String message) {
        ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED)
            .entity("{ "error": "" + message + "" }")
            .type(MediaType.APPLICATION_JSON)
            .build());
    }
}

Part 3 — Role-Based Authorization with @RolesAllowed

Register Jersey’s built-in RolesAllowedDynamicFeature and then annotate resource methods with @RolesAllowed:

// Register in your ResourceConfig
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;

@ApplicationPath("/api")
public class AppConfig extends ResourceConfig {
    public AppConfig() {
        packages("com.ankurm");
        register(RolesAllowedDynamicFeature.class);  // enables @RolesAllowed
    }
}

// Secured resource class
@Path("/orders")
public class OrderResource {

    @GET
    @PermitAll                         // any authenticated user
    @Produces(MediaType.APPLICATION_JSON)
    public Response listOrders(@Context SecurityContext sc) {
        return Response.ok("{ "user": "" + sc.getUserPrincipal().getName() + "" }").build();
    }

    @DELETE
    @Path("/{id}")
    @RolesAllowed("ADMIN")             // only ADMIN role
    public Response deleteOrder(@PathParam("id") long id) {
        return Response.noContent().build();
    }

    @POST
    @RolesAllowed({"USER", "ADMIN"})   // USER or ADMIN
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createOrder(String body) {
        return Response.status(201).entity(body).build();
    }
}

Part 4 — Issuing a JWT Token (Login Endpoint)

@Path("/auth")
public class AuthResource {

    private static final Algorithm ALGO = Algorithm.HMAC256("change-me-in-prod-use-env-var");

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(LoginRequest req) {
        // Validate credentials (replace with real user store)
        if (!"alice".equals(req.username()) || !"s3cr3t".equals(req.password())) {
            return Response.status(401).entity("{ "error": "Bad credentials" }").build();
        }

        String token = JWT.create()
            .withSubject(req.username())
            .withClaim("roles", List.of("USER"))
            .withIssuedAt(new Date())
            .withExpiresAt(new Date(System.currentTimeMillis() + 3_600_000)) // 1 h
            .sign(ALGO);

        return Response.ok("{ "token": "" + token + "" }").build();
    }

    public record LoginRequest(String username, String password) {}
}

Testing with curl

# 1. Obtain a JWT
curl -s -X POST http://localhost:8080/api/auth/login 
     -H 'Content-Type: application/json' 
     -d '{"username":"alice","password":"s3cr3t"}'
# {"token":"eyJ..."}

# 2. Call a protected endpoint
TOKEN="eyJ..."
curl -s http://localhost:8080/api/orders 
     -H "Authorization: Bearer $TOKEN"

# 3. Try an admin-only endpoint as alice (should fail)
curl -s -X DELETE http://localhost:8080/api/orders/42 
     -H "Authorization: Bearer $TOKEN"
# HTTP 403 Forbidden

Security Best Practices

  • Always use HTTPS — Basic Auth and JWTs are only safe over TLS. Never expose them over plain HTTP.
  • Short-lived tokens — set JWT expiry to 15–60 minutes and implement refresh tokens for long sessions.
  • Store secrets in environment variables — never hardcode the JWT signing secret in source code.
  • Hash passwords with bcrypt — never compare plaintext passwords; use BCrypt or Argon2.
  • Validate all JWT claims — always check exp, iss, and aud in production.

See Also

Conclusion

Securing Jersey REST APIs requires two layers: an authentication filter that identifies the caller and injects a SecurityContext, and an authorization layer that checks roles before granting access to resource methods. HTTP Basic is simple to implement and fine for internal APIs or development environments; JWT Bearer tokens are the right choice for public-facing, stateless services. Pair either approach with RolesAllowedDynamicFeature and @RolesAllowed annotations for clean, declarative access control without any boilerplate in your resource classes.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.