Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support specifying multiple tenants in @TenantFeature #40525

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,54 +1,21 @@
package io.quarkus.oidc;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;

/**
* Qualifier used to specify which named tenant is associated with one or more OIDC feature.
* Annotation used to specify which named tenants are associated with an OIDC feature.
*/
@Target({ METHOD, FIELD, PARAMETER, TYPE })
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface TenantFeature {
/**
* Identifies an OIDC tenant to which a given feature applies.
* Identifies one or more OIDC tenants to which a given feature applies.
*/
String value();

/**
* Supports inline instantiation of the {@link TenantFeature} qualifier.
*/
final class TenantFeatureLiteral extends AnnotationLiteral<TenantFeature> implements TenantFeature {

private final String value;

private TenantFeatureLiteral(String value) {
this.value = value;
}

@Override
public String value() {
return value;
}

@Override
public String toString() {
return "TenantFeatureLiteral [value=" + value + "]";
}

public static TenantFeature of(String value) {
return new TenantFeatureLiteral(value);
}
}
String[] value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class OidcProvider implements Closeable {
final RefreshableVerificationKeyResolver asymmetricKeyResolver;
final DynamicVerificationKeyResolver keyResolverProvider;
final OidcTenantConfig oidcConfig;
final TokenCustomizer tokenCustomizer;
final List<TokenCustomizer> tokenCustomizers;
final String issuer;
final String[] audience;
final Map<String, String> requiredClaims;
Expand All @@ -85,10 +85,10 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json
}

public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks,
TokenCustomizer tokenCustomizer, Key tokenDecryptionKey, List<Validator> customValidators) {
List<TokenCustomizer> tokenCustomizers, Key tokenDecryptionKey, List<Validator> customValidators) {
this.client = client;
this.oidcConfig = oidcConfig;
this.tokenCustomizer = tokenCustomizer;
this.tokenCustomizers = tokenCustomizers;
if (jwks != null) {
this.asymmetricKeyResolver = new JsonWebKeyResolver(jwks, oidcConfig.token.forcedJwkRefreshInterval);
} else if (oidcConfig != null && oidcConfig.certificateChain.trustStoreFile.isPresent()) {
Expand All @@ -113,7 +113,7 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json
public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) {
this.client = null;
this.oidcConfig = oidcConfig;
this.tokenCustomizer = TenantFeatureFinder.find(oidcConfig);
this.tokenCustomizers = TenantFeatureFinder.find(oidcConfig);
if (publicKeyEnc != null) {
this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc);
} else if (oidcConfig.certificateChain.trustStoreFile.isPresent()) {
Expand Down Expand Up @@ -274,17 +274,18 @@ private TokenVerificationResult verifyJwtTokenInternal(String token,
}

private String customizeJwtToken(String token) {
if (tokenCustomizer != null) {
JsonObject headers = AbstractJsonObjectResponse.toJsonObject(
OidcUtils.decodeJwtHeadersAsString(token));
headers = tokenCustomizer.customizeHeaders(headers);
if (headers != null) {
String newHeaders = new String(
Base64.getUrlEncoder().withoutPadding().encode(headers.toString().getBytes()),
StandardCharsets.UTF_8);
int dotIndex = token.indexOf('.');
String newToken = newHeaders + token.substring(dotIndex);
return newToken;
if (tokenCustomizers != null) {
for (TokenCustomizer tokenCustomizer : tokenCustomizers) {
JsonObject headers = AbstractJsonObjectResponse.toJsonObject(OidcUtils.decodeJwtHeadersAsString(token));
headers = tokenCustomizer.customizeHeaders(headers);
if (headers != null) {
String newHeaders = new String(
Base64.getUrlEncoder().withoutPadding().encode(headers.toString().getBytes()),
StandardCharsets.UTF_8);
int dotIndex = token.indexOf('.');
String newToken = newHeaders + token.substring(dotIndex);
return newToken;
}
}
}
return token;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package io.quarkus.oidc.runtime;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import jakarta.enterprise.inject.Default;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.ClientProxy;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.TenantFeature.TenantFeatureLiteral;
import io.quarkus.oidc.TokenCustomizer;

public class TenantFeatureFinder {
Expand All @@ -20,47 +21,56 @@ private TenantFeatureFinder() {

}

public static TokenCustomizer find(OidcTenantConfig oidcConfig) {
public static List<TokenCustomizer> find(OidcTenantConfig oidcConfig) {
if (oidcConfig == null) {
return null;
return List.of();
}
ArcContainer container = Arc.container();
if (container != null) {
String customizerName = oidcConfig.token.customizerName.orElse(null);
if (customizerName != null && !customizerName.isEmpty()) {
InstanceHandle<TokenCustomizer> tokenCustomizer = container.instance(customizerName);
if (tokenCustomizer.isAvailable()) {
return tokenCustomizer.get();
return List.of(tokenCustomizer.get());
} else {
throw new OIDCException("Unable to find TokenCustomizer " + customizerName);
}
} else if (oidcConfig.tenantId.isPresent()) {
return container
.instance(TokenCustomizer.class, TenantFeature.TenantFeatureLiteral.of(oidcConfig.tenantId.get()))
.get();
} else {
return find(oidcConfig, TokenCustomizer.class);
}
}
return null;
return List.of();
}

public static <T> List<T> find(OidcTenantConfig oidcTenantConfig, Class<T> tenantFeatureClass) {
if (oidcTenantConfig != null && oidcTenantConfig.tenantId.isPresent()) {
var tenantsValidators = new ArrayList<T>();
for (var instance : Arc.container().listAll(tenantFeatureClass, Default.Literal.INSTANCE)) {
if (instance.isAvailable()) {
tenantsValidators.add(instance.get());
ArcContainer container = Arc.container();
if (container != null) {
var tenantsValidators = new ArrayList<T>();
for (var instance : container.listAll(tenantFeatureClass, Default.Literal.INSTANCE)) {
if (instance.isAvailable()) {
tenantsValidators.add(instance.get());
}
}
}
for (var instance : Arc.container().listAll(tenantFeatureClass,
TenantFeatureLiteral.of(oidcTenantConfig.tenantId.get()))) {
if (instance.isAvailable()) {
tenantsValidators.add(instance.get());
tenantsValidators
.addAll(findTenantFeaturesByTenantId(tenantFeatureClass, oidcTenantConfig.tenantId.get(), container));
if (!tenantsValidators.isEmpty()) {
return List.copyOf(tenantsValidators);
}
}
if (!tenantsValidators.isEmpty()) {
return List.copyOf(tenantsValidators);
}
}
return List.of();
}

private static <T> List<T> findTenantFeaturesByTenantId(Class<T> tenantFeatureClass, String tenantId,
ArcContainer container) {
List<T> list = new ArrayList<>();
for (T tenantFeature : container.listAll(tenantFeatureClass).stream().map(InstanceHandle::get).toList()) {
TenantFeature annotation = ClientProxy.unwrap(tenantFeature).getClass().getAnnotation(TenantFeature.class);
if (annotation != null && Arrays.asList(annotation.value()).contains(tenantId)) {
list.add(tenantFeature);
}
}
return list;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ public void testAlgorithmCustomizer() throws Exception {
}
}

try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, new TokenCustomizer() {
try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, List.of(new TokenCustomizer() {

@Override
public JsonObject customizeHeaders(JsonObject headers) {
return Json.createObjectBuilder(headers).add("alg", "RS256").build();
}

}, null, null)) {
}), null, null)) {
TokenVerificationResult result = provider.verifyJwtToken(newToken, false, false, null);
assertEquals("http://keycloak/realm", result.localVerificationResult.getString("iss"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ public String admin() {
return "granted:" + identity.getRoles();
}

@Path("custombearer")
@GET
@RolesAllowed("admin")
@Produces(MediaType.APPLICATION_JSON)
public String customBearerAdmin() {
return "granted:" + identity.getRoles();
}

@Path("bearer-required-algorithm")
@GET
@RolesAllowed("admin")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.it.keycloak;

import jakarta.inject.Singleton;
import jakarta.json.Json;
import jakarta.json.JsonObject;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.TokenCustomizer;

@Singleton
@TenantFeature({ "bearer", "custombearer" })
@Unremovable
public class BearerTenantsTokenCustomizer implements TokenCustomizer {

volatile int rs256CountBearer;
volatile int rs256CountCustomBearer;

@Override
public JsonObject customizeHeaders(JsonObject headers) {
String customizeHeader = null;
if (headers.containsKey("customize_bearer")) {
customizeHeader = "customize_bearer";
} else if (headers.containsKey("customize_custombearer")) {
customizeHeader = "customize_custombearer";
}
if (customizeHeader == null) {
return null;
}
String alg = headers.getString("alg");
if ("RS256".equals(alg)) {
if ("customize_bearer".equals(customizeHeader)) {
if (0 == rs256CountBearer++) {
return null;
} else {
return Json.createObjectBuilder(headers).remove(customizeHeader).build();
}
} else {
if (0 == rs256CountCustomBearer++) {
return null;
} else {
return Json.createObjectBuilder(headers).remove(customizeHeader).build();
}
}
} else if ("RS384".equals(alg)) {
return null;
} else if ("RS512".equals(alg)) {
return Json.createObjectBuilder(headers).add("alg", "RS256").build();
}
return null;
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ quarkus.oidc.bearer.credentials.secret=secret
quarkus.oidc.bearer.token.audience=https://service.example.com
quarkus.oidc.bearer.allow-token-introspection-cache=false

quarkus.oidc.custombearer.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.custombearer.client-id=quarkus-app
quarkus.oidc.custombearer.credentials.secret=secret
quarkus.oidc.custombearer.token.audience=https://service.example.com
quarkus.oidc.custombearer.allow-token-introspection-cache=false

quarkus.oidc.bearer-kid-or-chain.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.bearer-kid-or-chain.client-id=quarkus-app
quarkus.oidc.bearer-kid-or-chain.credentials.secret=secret
Expand Down
Loading