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, andaudin production.
See Also
- Handling Exceptions in JAX-RS Jersey with ExceptionMapper
- Jersey REST API – Complete File Upload Example
- Building a REST API with Spring Boot
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.