Add JWT Bearer auth mechanism to populate Quarkus SecurityIdentity

This commit is contained in:
a
2026-06-13 09:33:27 +01:00
parent 69b6a49e3a
commit 96ad92cec9
2 changed files with 147 additions and 0 deletions
@@ -0,0 +1,61 @@
package stirling.software.proprietary.security.identity;
import java.util.Collections;
import java.util.Set;
import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
/**
* Quarkus HTTP authentication mechanism for {@code Authorization: Bearer <jwt>} credentials. It
* delegates token validation to {@link JwtTokenIdentityProvider}. When no Bearer token is present
* it yields no identity (anonymous), so unauthenticated/open paths (e.g. endpoints when login is
* disabled, or X-API-KEY auth) are not disturbed.
*/
@ApplicationScoped
public class JwtBearerAuthenticationMechanism implements HttpAuthenticationMechanism {
private static final String BEARER_PREFIX = "Bearer ";
@Override
public Uni<SecurityIdentity> authenticate(
RoutingContext context, IdentityProviderManager identityProviderManager) {
String token = bearerToken(context);
if (token == null) {
return Uni.createFrom().nullItem();
}
return identityProviderManager.authenticate(
new TokenAuthenticationRequest(new TokenCredential(token, "bearer")));
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "Bearer"));
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Collections.singleton(TokenAuthenticationRequest.class);
}
private static String bearerToken(RoutingContext context) {
String authz = context.request().getHeader("Authorization");
if (authz != null
&& authz.regionMatches(true, 0, BEARER_PREFIX, 0, BEARER_PREFIX.length())) {
String token = authz.substring(BEARER_PREFIX.length()).trim();
if (!token.isEmpty()) {
return token;
}
}
return null;
}
}
@@ -0,0 +1,86 @@
package stirling.software.proprietary.security.identity;
import java.util.Map;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.security.service.JwtServiceInterface;
/**
* Validates a Bearer {@link TokenAuthenticationRequest} with the application's {@link
* JwtServiceInterface} (jjwt + keystore) and produces a Quarkus {@link SecurityIdentity}. This
* replaces the Spring Security JWT filter that previously populated the SecurityContext; the {@code
* role} claim (e.g. {@code ROLE_ADMIN}) is mapped to Quarkus roles, with the {@code ROLE_} prefix
* also stripped so {@code @RolesAllowed("ADMIN")} matches.
*/
@Slf4j
@ApplicationScoped
public class JwtTokenIdentityProvider implements IdentityProvider<TokenAuthenticationRequest> {
private static final String ROLE_PREFIX = "ROLE_";
@Inject JwtServiceInterface jwtService;
@Override
public Class<TokenAuthenticationRequest> getRequestType() {
return TokenAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(
TokenAuthenticationRequest request, AuthenticationRequestContext context) {
String token = request.getToken().getToken();
// Token validation reads the keystore and parses claims (blocking work), so run it off the
// IO thread.
return context.runBlocking(() -> buildIdentity(token));
}
private SecurityIdentity buildIdentity(String token) {
try {
jwtService.validateToken(token);
String username = jwtService.extractUsername(token);
if (username == null || username.isBlank()) {
throw new AuthenticationFailedException("JWT has no subject");
}
QuarkusSecurityIdentity.Builder builder =
QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username));
Map<String, Object> claims = jwtService.extractClaims(token);
Object role = claims == null ? null : claims.get("role");
if (role != null) {
for (String raw : role.toString().split(",")) {
String r = raw.trim();
if (r.isEmpty()) {
continue;
}
builder.addRole(r);
if (r.startsWith(ROLE_PREFIX)) {
builder.addRole(r.substring(ROLE_PREFIX.length()));
}
}
}
if (claims != null) {
builder.addAttribute("claims", claims);
}
return builder.build();
} catch (AuthenticationFailedException e) {
throw e;
} catch (Exception e) {
log.debug("JWT authentication failed: {}", e.getMessage());
throw new AuthenticationFailedException(e);
}
}
}