Jakarta Security
Only Anna, the administrator, should delete blog posts, while any signed-in author may view their profile. Jakarta Security solves two tasks. Authentication finds out who came: it checks the name and password. Authorization decides what that user may do: usually by roles, for example admin and author.
Login method and roles
// src/main/java/com/example/blog/SecurityConfig.java
package com.example.blog;
import jakarta.annotation.security.DeclareRoles;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition;
@BasicAuthenticationMechanismDefinition(realmName = "blog")
@DeclareRoles({"admin", "author"})
@ApplicationScoped
public class SecurityConfig {
}@BasicAuthenticationMechanismDefinition enables HTTP Basic: the client sends the name and password in the Authorization header with every request. @DeclareRoles lists the application roles.
The user store
An IdentityStore checks the name and password and reports the user's groups. Groups with the same names as roles become roles.
// src/main/java/com/example/blog/BlogIdentityStore.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.security.enterprise.credential.UsernamePasswordCredential;
import jakarta.security.enterprise.identitystore.CredentialValidationResult;
import jakarta.security.enterprise.identitystore.IdentityStore;
import java.util.Map;
import java.util.Set;
@ApplicationScoped
public class BlogIdentityStore implements IdentityStore {
private record User(String password, Set<String> groups) {
}
// for the learning example only: a real application keeps users in a database
private final Map<String, User> users = Map.of(
"anna", new User("changeme", Set.of("admin", "author")),
"oleg", new User("changeme", Set.of("author")));
public CredentialValidationResult validate(UsernamePasswordCredential credential) {
User user = users.get(credential.getCaller());
if (user == null || !credential.compareTo(credential.getCaller(), user.password())) {
return CredentialValidationResult.INVALID_RESULT;
}
return new CredentialValidationResult(credential.getCaller(), user.groups());
}
}The server finds the bean that implements IdentityStore and calls its validate method for every login. A real application keeps password hashes in a database — there is a ready-made @DatabaseIdentityStoreDefinition store for that.
Protecting methods
Let's allow deleting a post only for the admin role:
// src/main/java/com/example/blog/PostResource.java (fragment)
@DELETE
@Path("/{id}")
@RolesAllowed("admin")
public void delete(@PathParam("id") long id) {
repository.delete(id);
}The @RolesAllowed annotation comes from the jakarta.annotation.security package. Methods without it stay open.
Who is signed in
The SecurityContext from Jakarta Security tells you about the current user:
// src/main/java/com/example/blog/MeResource.java
package com.example.blog;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.security.enterprise.SecurityContext;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/me")
@Produces(MediaType.APPLICATION_JSON)
public class MeResource {
public record Me(String name, boolean admin) {
}
@Inject
SecurityContext security;
@GET
@RolesAllowed({"admin", "author"})
public Me me() {
return new Me(security.getCallerPrincipal().getName(),
security.isCallerInRole("admin"));
}
}Trying it out
The curl flag -u name:password sends a Basic header:
curl -i http://localhost:8080/blog/api/me
curl -u anna:changeme http://localhost:8080/blog/api/me
curl -u oleg:changeme http://localhost:8080/blog/api/me
curl -i -u oleg:changeme -X DELETE http://localhost:8080/blog/api/posts/2
curl -i -u anna:changeme -X DELETE http://localhost:8080/blog/api/posts/2HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="blog"
{"admin":true,"name":"anna"}
{"admin":false,"name":"oleg"}
HTTP/1.1 403 Forbidden
HTTP/1.1 204 No Content
| Code | Meaning |
|---|---|
401 Unauthorized | No credentials or a wrong password |
403 Forbidden | The user is known but lacks the role |
Oleg signed in successfully, but he may not delete posts — hence 403. Anna may.
Common mistake
Enabling Basic over plain HTTP in a production system. The header Authorization: Basic YW5uYTpjaGFuZ2VtZQ== is just the name and password in Base64 encoding, not encryption: anyone who sees the traffic can read the password. Basic is acceptable only over HTTPS — Open Liberty already listens on port 8443.