From 09bfbfb674f696ebc313e66859219b4670861a59 Mon Sep 17 00:00:00 2001 From: Andrej Petras Date: Sun, 21 Jan 2024 22:46:35 +0100 Subject: [PATCH] feat: add principal token to the application context (#123) * feat: add token parser service, add principal token to application context * feat: add context principal token --- extensions/context/README.md | 9 +- extensions/context/pom.xml | 4 + .../org/tkit/quarkus/context/Context.java | 55 ++++++++- extensions/rest-context/README.md | 65 ++++++++--- .../context/PrincipalRequiredException.java | 13 +++ .../PrincipalTokenRequiredException.java | 13 +++ .../quarkus/rs/context/RestContextConfig.java | 43 +++++++ .../rs/context/RestContextInterceptor.java | 48 +++++++- .../DefaultPrincipalNameCustomResolver.java | 19 +++ .../PrincipalNameCustomResolver.java | 10 ++ .../principal/RestContextPrincipalConfig.java | 46 ++++---- .../RestContextPrincipalResolverService.java | 89 +++++---------- ....java => DefaultTenantCustomResolver.java} | 6 +- .../tenant/RestContextTenantIdConfig.java | 26 ++++- .../RestContextTenantResolverService.java | 45 +++----- .../tenant/RestCustomTenantResolver.java | 8 -- .../context/tenant/TenantCustomResolver.java | 10 ++ .../rs/context/token/TokenClaimUtility.java | 108 ++++++++++++++++++ .../rs/context/token/TokenException.java | 15 +++ .../rs/context/token/TokenParserRequest.java | 74 ++++++++++++ .../rs/context/token/TokenParserService.java | 68 +++++++++++ pom.xml | 2 +- 22 files changed, 626 insertions(+), 150 deletions(-) create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalRequiredException.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalTokenRequiredException.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/DefaultPrincipalNameCustomResolver.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/PrincipalNameCustomResolver.java rename extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/{DefaultRestCustomTenantResolver.java => DefaultTenantCustomResolver.java} (57%) delete mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestCustomTenantResolver.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/TenantCustomResolver.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenClaimUtility.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenException.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserRequest.java create mode 100644 extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserService.java diff --git a/extensions/context/README.md b/extensions/context/README.md index 5df667d..35a5ec4 100644 --- a/extensions/context/README.md +++ b/extensions/context/README.md @@ -8,4 +8,11 @@ Maven dependency org.tkit.quarkus.lib tkit-quarkus-context -``` \ No newline at end of file +``` + +Context object holds + +* request ID +* principal name +* principal token +* business values \ No newline at end of file diff --git a/extensions/context/pom.xml b/extensions/context/pom.xml index 755efc2..4f21d72 100644 --- a/extensions/context/pom.xml +++ b/extensions/context/pom.xml @@ -18,5 +18,9 @@ quarkus-arc provided + + org.eclipse.microprofile.jwt + microprofile-jwt-auth-api + diff --git a/extensions/context/src/main/java/org/tkit/quarkus/context/Context.java b/extensions/context/src/main/java/org/tkit/quarkus/context/Context.java index 4230a94..abe491c 100644 --- a/extensions/context/src/main/java/org/tkit/quarkus/context/Context.java +++ b/extensions/context/src/main/java/org/tkit/quarkus/context/Context.java @@ -2,6 +2,8 @@ import java.util.*; +import org.eclipse.microprofile.jwt.JsonWebToken; + public class Context { /** @@ -36,7 +38,10 @@ public class Context { */ private final Stack errors; - Context(String correlationId, String businessContext, String principal, Map meta, String tenantId) { + private final JsonWebToken principalToken; + + Context(String correlationId, String businessContext, String principal, Map meta, String tenantId, + JsonWebToken principalToken) { this.correlationId = correlationId; this.businessContext = businessContext; this.meta = meta; @@ -44,6 +49,25 @@ public class Context { this.businessParams = new HashSet<>(); this.errors = new Stack<>(); this.tenantId = tenantId; + this.principalToken = principalToken; + } + + /** + * Gets the principal token + * + * @return the principal token + */ + public JsonWebToken getPrincipalToken() { + return principalToken; + } + + /** + * Returns {@code true} if principal token is set in context. + * + * @return {@code true} if principal token is set in context. + */ + public boolean hasPrincipalToken() { + return principalToken != null; } /** @@ -55,6 +79,15 @@ public String getPrincipal() { return principal; } + /** + * Returns {@code true} if principal is set in context. + * + * @return {@code true} if principal is set in context. + */ + public boolean hasPrincipal() { + return principal != null; + } + /** * Gets business context. * @@ -73,6 +106,15 @@ public String getTenantId() { return tenantId; } + /** + * Returns {@code true} if tenant ID is set in context. + * + * @return {@code true} if tenant ID is set in context. + */ + public boolean hasTenantId() { + return tenantId != null; + } + /** * Gets the correlation ID. * @@ -170,6 +212,8 @@ public static class ApplicationContextBuilder { private final Map meta = new HashMap<>(); + private JsonWebToken principalToken; + public ApplicationContextBuilder correlationId(String correlationId) { if (correlationId == null) { return this; @@ -178,6 +222,11 @@ public ApplicationContextBuilder correlationId(String correlationId) { return this; } + public ApplicationContextBuilder principalToken(JsonWebToken principalToken) { + this.principalToken = principalToken; + return this; + } + public ApplicationContextBuilder principal(String principal) { this.principal = principal; return this; @@ -199,7 +248,7 @@ public ApplicationContextBuilder addMeta(String key, String value) { } public Context build() { - return new Context(correlationId, businessContext, principal, meta, tenantId); + return new Context(correlationId, businessContext, principal, meta, tenantId, principalToken); } } -} \ No newline at end of file +} diff --git a/extensions/rest-context/README.md b/extensions/rest-context/README.md index 3d66d97..f93411e 100644 --- a/extensions/rest-context/README.md +++ b/extensions/rest-context/README.md @@ -26,27 +26,55 @@ tkit.rs.context.correlation-id.header-param-name=X-Correlation-ID tkit.rs.context.business-context.enabled=false # Business context header parameter tkit.rs.context.business-context.header-param-name=business-context +# Add token to the context +tkit.rs.context.token-context=true +# Principal token is mandatory +tkit.rs.context.token-mandatory=false +# Principal is mandatory +tkit.rs.context.principal-mandatory=false +# Enable or disable token parser service +tkit.rs.context.token.enabled=true +# Make principal token required +tkit.rs.context.token.required=true +# Token type name for parsing token +tkit.rs.context.token.type=principal-token +# Verify the token or skip token verification +tkit.rs.context.token.verify=false +# Enable or disable the public key location for the verified token. +tkit.rs.context.token.public-key-location.enabled=false +# Token public key location suffix. This property is use only if public-key-location.enabled set to true. +tkit.rs.context.token.public-key-location.suffix=/protocol/openid-connect/certs +# Token header parameter +tkit.rs.context.token.header-param=apm-principal-token + ``` -Runtime principal configuration. +Runtime principal name configuration. ```properties # Enable or disable principal name for the context -tkit.rs.context.principal.enabled=true +tkit.rs.context.principal.name.enabled=true +# Enable or disable custom principal service +tkit.rs.context.principal.name.custom-service-enabled=false # Enabled or disable `SecurityContext` principal name resolver -tkit.rs.context.principal.security-context.enabled=false +tkit.rs.context.principal.name.security-context.enabled=false +# Optional default principal name, default null +tkit.rs.context.principal.name.default= +# Read name from token +tkit.rs.context.principal.name.token-enabled=true +# Token claim name for principal name +tkit.rs.context.principal.name.token-claim-name=sub +# Use header parameter as principal name +tkit.rs.context.principal.name.header-param-enabled=false +# Principal name header for header-param-enabled +tkit.rs.context.principal.name.header-param-name=x-principal-id +``` + +Runtime principal token configuration. + +```properties # Enabled or disable token resolver for principal name tkit.rs.context.principal.token.enabled=true -# Verify the token or skip token verification -tkit.rs.context.principal.token.verify=false -# Enable or disable the public key location for the verified token. -tkit.rs.context.principal.token.public-key-location.enabled=false -# Token public key location suffix. This property is use only if public-key-location.enabled set to true. -tkit.rs.context.principal.token.public-key-location.suffix=/protocol/openid-connect/certs -# Token header parameter -tkit.rs.context.principal.token.token-header-param=apm-principal-token -# Token claim name for principal name -tkit.rs.context.principal.token.claim-name=sub ``` Runtime tenant configuration. @@ -56,10 +84,19 @@ Runtime tenant configuration. tkit.rs.context.tenant-id.enabled=false # default tenant ID tkit.rs.context.tenant-id.default=default +# Use token claim to setup tenant ID +tkit.rs.context.tenant-id.token.enabled=false +# Token claim parameter for tenant ID +tkit.rs.context.tenant-id.token.claim-tenant-param=tenantId # enable or disable tenant ID from header parameter tkit.rs.context.tenant-id.header-param-enabled=false # header parameter of the tenant ID tkit.rs.context.tenant-id.header-param-name=tenant-id +``` + +Tenant mock service for testing + +```properties # enable or disable tenant ID mock service tkit.rs.context.tenant-id.mock.enabled=false # default tenant ID @@ -68,8 +105,6 @@ tkit.rs.context.tenant-id.mock.default-tenant=default tkit.rs.context.tenant-id.mock.data.= # token claim parameter of organization ID tkit.rs.context.tenant-id.mock.claim-org-id=orgId -# token header parameter -tkit.rs.context.tenant-id.mock.token-header-param=apm-principal-token ``` Priority of the tenant ID resolver: diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalRequiredException.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalRequiredException.java new file mode 100644 index 0000000..abf4e05 --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalRequiredException.java @@ -0,0 +1,13 @@ +package org.tkit.quarkus.rs.context; + +public class PrincipalRequiredException extends RestContextException { + + public PrincipalRequiredException() { + super(ErrorKeys.PRINCIPAL_REQUIRED, "Principal is required"); + } + + public enum ErrorKeys { + + PRINCIPAL_REQUIRED; + } +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalTokenRequiredException.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalTokenRequiredException.java new file mode 100644 index 0000000..7044ad7 --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/PrincipalTokenRequiredException.java @@ -0,0 +1,13 @@ +package org.tkit.quarkus.rs.context; + +public class PrincipalTokenRequiredException extends RestContextException { + + public PrincipalTokenRequiredException() { + super(ErrorKeys.PRINCIPAL_TOKEN_REQUIRED, "Principal token is required"); + } + + public enum ErrorKeys { + + PRINCIPAL_TOKEN_REQUIRED; + } +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextConfig.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextConfig.java index 89096ec..ed83787 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextConfig.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextConfig.java @@ -21,6 +21,21 @@ public interface RestContextConfig { @WithName("business-context") RestContextBusinessConfig businessContext(); + @WithName("token") + TokenConfig token(); + + @WithName("token-context") + @WithDefault("true") + boolean tokenContext(); + + @WithName("token-mandatory") + @WithDefault("false") + boolean tokenMandatory(); + + @WithName("principal-mandatory") + @WithDefault("false") + boolean principalMandatory(); + interface RestContextCorrelationIdConfig { @WithName("enabled") @@ -47,4 +62,32 @@ interface RestContextBusinessConfig { } + interface TokenConfig { + + @WithName("enabled") + @WithDefault("true") + boolean enabled(); + + @WithName("type") + @WithDefault("principal-token") + String type(); + + @WithName("verify") + @WithDefault("false") + boolean verify(); + + @WithName("public-key-location.enabled") + @WithDefault("false") + boolean issuerEnabled(); + + @WithName("public-key-location.suffix") + @WithDefault("/protocol/openid-connect/certs") + String issuerSuffix(); + + @WithName("header-param") + @WithDefault("apm-principal-token") + String tokenHeaderParam(); + + } + } diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextInterceptor.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextInterceptor.java index d159064..33ce4eb 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextInterceptor.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/RestContextInterceptor.java @@ -8,13 +8,17 @@ import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; -import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.ext.Provider; +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.tkit.quarkus.context.ApplicationContext; import org.tkit.quarkus.context.Context; import org.tkit.quarkus.rs.context.principal.RestContextPrincipalResolverService; import org.tkit.quarkus.rs.context.tenant.RestContextTenantResolverService; +import org.tkit.quarkus.rs.context.token.TokenParserRequest; +import org.tkit.quarkus.rs.context.token.TokenParserService; import io.quarkus.arc.Arc; @@ -22,8 +26,7 @@ @Priority(1) public class RestContextInterceptor implements ContainerRequestFilter, ContainerResponseFilter { - @jakarta.ws.rs.core.Context - SecurityContext securityContext; + private static final Logger log = LoggerFactory.getLogger(RestContextInterceptor.class); @Inject RestContextConfig config; @@ -34,6 +37,9 @@ public class RestContextInterceptor implements ContainerRequestFilter, Container @Inject RestContextTenantResolverService tenantResolverService; + @Inject + TokenParserService tokenParserService; + private final RestContextHeaderContainer headerContainer; public RestContextInterceptor() { @@ -41,7 +47,7 @@ public RestContextInterceptor() { } @Override - public void filter(ContainerRequestContext requestContext) throws IOException { + public void filter(ContainerRequestContext requestContext) { if (!config.enabled()) { return; @@ -66,17 +72,32 @@ public void filter(ContainerRequestContext requestContext) throws IOException { } } + // get principal token + JsonWebToken principalToken = getRestContextPrincipalToken(requestContext); + if (principalToken == null && config.tokenMandatory()) { + throw new PrincipalTokenRequiredException(); + } + // get principal ID - String principal = principalResolverService.getPrincipalName(requestContext); + String principal = principalResolverService.getPrincipalName(principalToken, requestContext); + if (principal == null && config.principalMandatory()) { + throw new PrincipalRequiredException(); + } // get tenant ID - String tenantId = tenantResolverService.getTenantId(requestContext); + String tenantId = tenantResolverService.getTenantId(principalToken, requestContext); + + // disable or enable token in the context + if (!config.tokenContext()) { + principalToken = null; + } // create a application context Context ctx = Context.builder() .correlationId(correlationId) .principal(principal) .tenantId(tenantId) + .principalToken(principalToken) .businessContext(businessContext) .build(); @@ -95,4 +116,19 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont // close application context ApplicationContext.close(); } + + private JsonWebToken getRestContextPrincipalToken(ContainerRequestContext containerRequestContext) { + var tokenConfig = config.token(); + if (!tokenConfig.enabled()) { + return null; + } + + String rawToken = containerRequestContext.getHeaders().getFirst(tokenConfig.tokenHeaderParam()); + TokenParserRequest request = new TokenParserRequest(rawToken) + .issuerEnabled(tokenConfig.issuerEnabled()) + .type(tokenConfig.type()) + .issuerSuffix(tokenConfig.issuerSuffix()); + return tokenParserService.parseToken(request); + } + } diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/DefaultPrincipalNameCustomResolver.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/DefaultPrincipalNameCustomResolver.java new file mode 100644 index 0000000..2cfee48 --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/DefaultPrincipalNameCustomResolver.java @@ -0,0 +1,19 @@ +package org.tkit.quarkus.rs.context.principal; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.container.ContainerRequestContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.arc.DefaultBean; +import io.quarkus.arc.Unremovable; + +@Unremovable +@DefaultBean +@ApplicationScoped +public class DefaultPrincipalNameCustomResolver implements PrincipalNameCustomResolver { + @Override + public String getPrincipalName(JsonWebToken principalToken, ContainerRequestContext containerRequestContext) { + return null; + } +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/PrincipalNameCustomResolver.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/PrincipalNameCustomResolver.java new file mode 100644 index 0000000..74076db --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/PrincipalNameCustomResolver.java @@ -0,0 +1,10 @@ +package org.tkit.quarkus.rs.context.principal; + +import jakarta.ws.rs.container.ContainerRequestContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +public interface PrincipalNameCustomResolver { + + String getPrincipalName(JsonWebToken principalToken, ContainerRequestContext containerRequestContext); +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalConfig.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalConfig.java index 7f3b62d..ebaf95b 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalConfig.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalConfig.java @@ -10,43 +10,41 @@ @StaticInitSafe @ConfigMapping(prefix = "tkit.rs.context.principal") public interface RestContextPrincipalConfig { - @WithName("enabled") - @WithDefault("true") - boolean enabled(); - @WithName("security-context") - SecurityContextConfig securityContext(); + @WithName("name") + PrincipalName name(); - @WithName("token") - TokenConfig token(); + interface PrincipalName { - @WithName("default") - Optional defaultPrincipal(); - - interface TokenConfig { @WithName("enabled") @WithDefault("true") boolean enabled(); - @WithName("verify") + @WithName("custom-service-enabled") @WithDefault("false") - boolean verify(); + boolean enabledCustomService(); - @WithName("public-key-location.enabled") - @WithDefault("false") - boolean issuerEnabled(); + @WithName("security-context") + SecurityContextConfig securityContext(); - @WithName("public-key-location.suffix") - @WithDefault("/protocol/openid-connect/certs") - boolean issuerSuffix(); + @WithName("default") + Optional defaultPrincipal(); - @WithName("token-header-param") - @WithDefault("apm-principal-token") - String tokenHeaderParam(); + @WithName("token-enabled") + @WithDefault("true") + boolean tokenEnabled(); - @WithName("claim-name") + @WithName("token-claim-name") @WithDefault("sub") - String claimName(); + String tokenClaimName(); + + @WithName("header-param-enabled") + @WithDefault("false") + boolean headerParamEnabled(); + + @WithName("header-param-name") + @WithDefault("x-principal-id") + String headerParamName(); } interface SecurityContextConfig { diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalResolverService.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalResolverService.java index 9a5a7eb..16bf3d6 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalResolverService.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/principal/RestContextPrincipalResolverService.java @@ -5,25 +5,12 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.SecurityContext; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.jwt.JwtClaims; -import org.jose4j.jwt.MalformedClaimException; -import org.jose4j.jwt.consumer.InvalidJwtException; -import org.jose4j.jwx.JsonWebStructure; -import org.jose4j.lang.JoseException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.tkit.quarkus.rs.context.RestContextException; -import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; -import io.smallrye.jwt.auth.principal.JWTParser; -import io.smallrye.jwt.auth.principal.ParseException; - @RequestScoped public class RestContextPrincipalResolverService { - private static final Logger log = LoggerFactory.getLogger(RestContextPrincipalResolverService.class); - @Inject RestContextPrincipalConfig config; @@ -31,18 +18,31 @@ public class RestContextPrincipalResolverService { SecurityContext securityContext; @Inject - JWTAuthContextInfo authContextInfo; + PrincipalNameCustomResolver customResolver; - @Inject - JWTParser parser; + public String getPrincipalName(JsonWebToken principalToken, ContainerRequestContext containerRequestContext) { - public String getPrincipalName(ContainerRequestContext containerRequestContext) { - if (!config.enabled()) { + var principalNameConfig = config.name(); + if (!principalNameConfig.enabled()) { return null; } + // get principal name from custom service + if (principalNameConfig.enabledCustomService()) { + try { + String principalName = customResolver.getPrincipalName(principalToken, containerRequestContext); + if (principalName != null && !principalName.isBlank()) { + return principalName; + } + } catch (Exception ex) { + throw new RestContextException(ErrorKeys.ERROR_CALL_CUSTOM_PRINCIPAL_NAME_SERVICE, + "Failed to call custom principal name resolver service, error: " + ex.getMessage(), ex); + } + } + // get principal name from the security context - if (config.securityContext().enabled() && securityContext != null && securityContext.getUserPrincipal() != null) { + if (principalNameConfig.securityContext().enabled() && securityContext != null + && securityContext.getUserPrincipal() != null) { String principal = securityContext.getUserPrincipal().getName(); if (principal != null && !principal.isBlank()) { return principal; @@ -50,56 +50,29 @@ public String getPrincipalName(ContainerRequestContext containerRequestContext) } // get the principal name from the token - if (config.token().enabled()) { - try { - String principal = getPrincipalNameFromToken( - containerRequestContext.getHeaders().getFirst(config.token().tokenHeaderParam())); + if (principalNameConfig.tokenEnabled()) { + // check principal name from token + if (principalToken != null) { + String principal = principalToken.getClaim(principalNameConfig.tokenClaimName()); if (principal != null && !principal.isBlank()) { return principal; } - } catch (Exception ex) { - log.error("Failed to verify/parse a token: {}", config.token().tokenHeaderParam()); - log.error(ex.getMessage(), ex); - throw new RestContextException(ErrorKeys.ERROR_PARSE_PRINCIPAL_TOKEN, - "Failed to verify/parse a token '" + config.token().tokenHeaderParam() + "', error: " + ex.getMessage(), - ex); } } - return config.defaultPrincipal().orElse(null); - } - - private String getPrincipalNameFromToken(String token) - throws InvalidJwtException, JoseException, MalformedClaimException, ParseException { - if (token == null) { - return null; - } - - String principal; - - var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(token); - var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload()); - - if (config.token().verify()) { - var info = authContextInfo; - - if (config.token().issuerEnabled()) { - var publicKeyLocation = jwtClaims.getIssuer() + config.token().issuerSuffix(); - info = new JWTAuthContextInfo(authContextInfo); - info.setPublicKeyLocation(publicKeyLocation); + // check principal name from header parameter + if (principalNameConfig.headerParamEnabled()) { + String tenantId = containerRequestContext.getHeaders().getFirst(principalNameConfig.headerParamName()); + if (tenantId != null && !tenantId.isBlank()) { + return tenantId; } - - var jwtWebToken = parser.parse(token, info); - principal = jwtWebToken.getClaim(config.token().claimName()); - } else { - principal = jwtClaims.getStringClaimValue(config.token().claimName()); } - return principal; + return principalNameConfig.defaultPrincipal().orElse(null); } public enum ErrorKeys { - ERROR_PARSE_PRINCIPAL_TOKEN; + ERROR_CALL_CUSTOM_PRINCIPAL_NAME_SERVICE; } } diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/DefaultRestCustomTenantResolver.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/DefaultTenantCustomResolver.java similarity index 57% rename from extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/DefaultRestCustomTenantResolver.java rename to extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/DefaultTenantCustomResolver.java index 1b609ad..75c90f4 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/DefaultRestCustomTenantResolver.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/DefaultTenantCustomResolver.java @@ -3,15 +3,17 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.container.ContainerRequestContext; +import org.eclipse.microprofile.jwt.JsonWebToken; + import io.quarkus.arc.DefaultBean; import io.quarkus.arc.Unremovable; @Unremovable @DefaultBean @ApplicationScoped -public class DefaultRestCustomTenantResolver implements RestCustomTenantResolver { +public class DefaultTenantCustomResolver implements TenantCustomResolver { @Override - public String getTenantId(ContainerRequestContext containerRequestContext) { + public String getTenantId(JsonWebToken principalToken, ContainerRequestContext containerRequestContext) { return null; } } diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantIdConfig.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantIdConfig.java index 31a313f..1527fbe 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantIdConfig.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantIdConfig.java @@ -30,6 +30,26 @@ public interface RestContextTenantIdConfig { @WithName("mock") MockConfig mock(); + @WithName("token") + TokenConfig token(); + + interface TokenConfig { + /** + * Enable or disable tenant token claim. + */ + @WithName("enabled") + @WithDefault("false") + boolean enabled(); + + /** + * Default mock tenant + */ + @WithName("claim-tenant-param") + @WithDefault("tenantId") + String claimTenantParam(); + + } + interface MockConfig { /** @@ -59,11 +79,5 @@ interface MockConfig { @WithDefault("orgId") String claimOrgId(); - /** - * Token header parameter for mock service - */ - @WithName("token-header-param") - @WithDefault("apm-principal-token") - String tokenHeaderParam(); } } diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantResolverService.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantResolverService.java index c060340..2a9089b 100644 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantResolverService.java +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestContextTenantResolverService.java @@ -4,9 +4,7 @@ import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; -import org.jose4j.jws.JsonWebSignature; -import org.jose4j.jwt.JwtClaims; -import org.jose4j.jwx.JsonWebStructure; +import org.eclipse.microprofile.jwt.JsonWebToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tkit.quarkus.rs.context.RestContextException; @@ -20,26 +18,34 @@ public class RestContextTenantResolverService { RestContextTenantIdConfig config; @Inject - RestCustomTenantResolver customTenantResolver; + TenantCustomResolver customTenantResolver; - public String getTenantId(ContainerRequestContext containerRequestContext) { + public String getTenantId(JsonWebToken principalToken, ContainerRequestContext containerRequestContext) { if (!config.enabled()) { return null; } // check mock service if (config.mock().enabled()) { - return getMockTenantId(containerRequestContext); + return getMockTenantId(principalToken); + } + + if (config.token().enabled()) { + // check principal name from token + if (principalToken != null) { + String tenantId = principalToken.getClaim(config.token().claimTenantParam()); + if (tenantId != null && !tenantId.isBlank()) { + return tenantId; + } + } } // get tenant-id from custom service try { - - String tenantId = customTenantResolver.getTenantId(containerRequestContext); + String tenantId = customTenantResolver.getTenantId(principalToken, containerRequestContext); if (tenantId != null && !tenantId.isBlank()) { return tenantId; } - } catch (Exception ex) { log.error("Failed to call custom tenant resolver service, error: " + ex.getMessage(), ex); throw new RestContextException(ErrorKeys.ERROR_CALL_CUSTOM_TENANT_SERVICE, @@ -57,23 +63,12 @@ public String getTenantId(ContainerRequestContext containerRequestContext) { return config.defaultTenantId(); } - private String getMockTenantId(ContainerRequestContext containerRequestContext) { + private String getMockTenantId(JsonWebToken principalToken) { var mockConfig = config.mock(); - String token = containerRequestContext.getHeaders().getFirst(mockConfig.tokenHeaderParam()); - if (token == null || token.isBlank()) { - return mockConfig.defaultTenant(); - } - - String organization; - try { - - var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(token); - var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload()); - organization = jwtClaims.getClaimValueAsString(config.mock().claimOrgId()); - } catch (Exception ex) { - throw new RestContextException(ErrorKeys.ERROR_MOCK_PARSE_TOKEN, "Error parse token. Error: " + ex.getMessage(), - ex); + String organization = null; + if (principalToken != null) { + organization = principalToken.getClaim(mockConfig.claimOrgId()); } if (organization == null) { @@ -90,8 +85,6 @@ private String getMockTenantId(ContainerRequestContext containerRequestContext) public enum ErrorKeys { - ERROR_MOCK_PARSE_TOKEN, - ERROR_CALL_CUSTOM_TENANT_SERVICE; } } diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestCustomTenantResolver.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestCustomTenantResolver.java deleted file mode 100644 index d2e155f..0000000 --- a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/RestCustomTenantResolver.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.tkit.quarkus.rs.context.tenant; - -import jakarta.ws.rs.container.ContainerRequestContext; - -public interface RestCustomTenantResolver { - - String getTenantId(ContainerRequestContext containerRequestContext); -} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/TenantCustomResolver.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/TenantCustomResolver.java new file mode 100644 index 0000000..977dadc --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/tenant/TenantCustomResolver.java @@ -0,0 +1,10 @@ +package org.tkit.quarkus.rs.context.tenant; + +import jakarta.ws.rs.container.ContainerRequestContext; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +public interface TenantCustomResolver { + + String getTenantId(JsonWebToken principalToken, ContainerRequestContext containerRequestContext); +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenClaimUtility.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenClaimUtility.java new file mode 100644 index 0000000..fbd8391 --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenClaimUtility.java @@ -0,0 +1,108 @@ +package org.tkit.quarkus.rs.context.token; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.smallrye.jwt.JsonUtils; + +/** + * Token claim utility + */ +public class TokenClaimUtility { + + /** + * Default string split separator + */ + public static final String DEFAULT_TOKEN_SEPARATOR = " "; + + private TokenClaimUtility() { + } + + /** + * Finds claim string list for path. + * + * @param token token web token + * @param path the path of claims + * @return the corresponding list of string or null + */ + public static List findClaimStringList(JsonWebToken token, String[] path) { + return findClaimStringList(token, path, DEFAULT_TOKEN_SEPARATOR); + } + + /** + * Finds claim string list for path. + * + * @param token token web token + * @param path the path of claims + * @param listSeparator split string value to list with this separator + * @return the corresponding list of string or null + */ + public static List findClaimStringList(JsonWebToken token, String[] path, String listSeparator) { + JsonValue claimValue = findClaimValue(token, path); + if (claimValue == null) { + return List.of(); + } + if (claimValue instanceof JsonArray jsonArray) { + return convertJsonArrayToList(jsonArray); + } + var tmp = claimValue.toString(); + if (tmp.isBlank()) { + return Collections.emptyList(); + } + return Arrays.asList(tmp.split(listSeparator)); + } + + /** + * Finds claim value for path. + * + * @param token token web token + * @param path the path of claims + * @return the corresponding list of string or null + */ + public static JsonValue findClaimValue(JsonWebToken token, String[] path) { + if (token == null) { + return null; + } + if (path == null || path.length == 0) { + return null; + } + return findClaimValue(JsonUtils.wrapValue(token.getClaim(path[0])), path, 1); + } + + private static JsonValue findClaimValue(JsonValue json, String[] path, int step) { + if (path == null) { + return json; + } + if (json == null) { + return null; + } + if (step >= path.length) { + return json; + } + if (json instanceof JsonObject) { + JsonValue claimValue = json.asJsonObject().get(path[step].replace("\"", "")); + return findClaimValue(claimValue, path, step + 1); + } + return json; + } + + public static List convertJsonArrayToList(JsonArray claimValue) { + List list = new ArrayList<>(claimValue.size()); + for (int i = 0; i < claimValue.size(); i++) { + String item = claimValue.getString(i); + if (item.isBlank()) { + continue; + } + list.add(item); + } + return list; + } +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenException.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenException.java new file mode 100644 index 0000000..57c2889 --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenException.java @@ -0,0 +1,15 @@ +package org.tkit.quarkus.rs.context.token; + +public class TokenException extends RuntimeException { + + private final Enum key; + + public TokenException(Enum key, String message, Throwable throwable) { + super(message, throwable); + this.key = key; + } + + public Enum getKey() { + return key; + } +} \ No newline at end of file diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserRequest.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserRequest.java new file mode 100644 index 0000000..78202ce --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserRequest.java @@ -0,0 +1,74 @@ +package org.tkit.quarkus.rs.context.token; + +public class TokenParserRequest { + + private final String rawToken; + + private boolean verify; + + private boolean issuerEnabled; + + private String issuerSuffix; + + private String type; + + public TokenParserRequest(String rawToken) { + this.rawToken = rawToken; + } + + public String getRawToken() { + return rawToken; + } + + public boolean isIssuerEnabled() { + return issuerEnabled; + } + + public void setIssuerEnabled(boolean issuerEnabled) { + this.issuerEnabled = issuerEnabled; + } + + public TokenParserRequest issuerEnabled(boolean issuerEnabled) { + setIssuerEnabled(issuerEnabled); + return this; + } + + public String getIssuerSuffix() { + return issuerSuffix; + } + + public void setIssuerSuffix(String issuerSuffix) { + this.issuerSuffix = issuerSuffix; + } + + public TokenParserRequest issuerSuffix(String issuerSuffix) { + setIssuerSuffix(issuerSuffix); + return this; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public TokenParserRequest type(String type) { + setType(type); + return this; + } + + public boolean isVerify() { + return verify; + } + + public void setVerify(boolean verify) { + this.verify = verify; + } + + public TokenParserRequest verify(boolean verify) { + setVerify(verify); + return this; + } +} diff --git a/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserService.java b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserService.java new file mode 100644 index 0000000..e974b7e --- /dev/null +++ b/extensions/rest-context/src/main/java/org/tkit/quarkus/rs/context/token/TokenParserService.java @@ -0,0 +1,68 @@ +package org.tkit.quarkus.rs.context.token; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.jwt.JsonWebToken; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.lang.JoseException; + +import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal; +import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; +import io.smallrye.jwt.auth.principal.JWTParser; +import io.smallrye.jwt.auth.principal.ParseException; + +@RequestScoped +public class TokenParserService { + + @Inject + JWTAuthContextInfo authContextInfo; + + @Inject + JWTParser parser; + + public JsonWebToken parseToken(TokenParserRequest request) throws TokenException { + if (request == null) { + return null; + } + try { + return parseTokenRequest(request); + } catch (Exception ex) { + throw new TokenException(ErrorKeys.ERROR_PARSE_TOKEN, "Error parse raw token", ex); + } + } + + private JsonWebToken parseTokenRequest(TokenParserRequest request) + throws InvalidJwtException, JoseException, MalformedClaimException, ParseException { + + if (request.getRawToken() == null) { + return null; + } + + if (request.isVerify()) { + var info = authContextInfo; + + if (request.isIssuerEnabled()) { + var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(request.getRawToken()); + var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload()); + var publicKeyLocation = jwtClaims.getIssuer() + request.getIssuerSuffix(); + info = new JWTAuthContextInfo(authContextInfo); + info.setPublicKeyLocation(publicKeyLocation); + } + return parser.parse(request.getRawToken(), info); + } + + var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(request.getRawToken()); + var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload()); + return new DefaultJWTCallerPrincipal(request.getRawToken(), request.getType(), jwtClaims); + } + + public enum ErrorKeys { + + ERROR_PARSE_TOKEN; + } +} diff --git a/pom.xml b/pom.xml index e04035c..29eb71a 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ - 3.6.4 + 3.6.6 6.4.2.Final 1.2.0 3.1.6