io.quarkus.oidc.runtime.OidcRecorder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of quarkus-oidc Show documentation
Show all versions of quarkus-oidc Show documentation
Secure your applications with OpenID Connect Adapter and IDP such as Keycloak
package io.quarkus.oidc.runtime;
import static io.quarkus.oidc.SecurityEvent.AUTH_SERVER_URL;
import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_AVAILABLE;
import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_NOT_AVAILABLE;
import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID;
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute;
import java.security.Key;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.PublicJsonWebKey;
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
import io.quarkus.oidc.OidcTenantConfig.TokenStateManager.Strategy;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.quarkus.oidc.common.runtime.OidcCommonConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.mutiny.ext.web.client.WebClient;
@Recorder
public class OidcRecorder {
private static final Logger LOG = Logger.getLogger(OidcRecorder.class);
private static final String SECURITY_EVENTS_ENABLED_CONFIG_KEY = "quarkus.security.events.enabled";
private static final Map dynamicTenantsConfig = new ConcurrentHashMap<>();
private static final Set tenantsExpectingServerAvailableEvents = ConcurrentHashMap.newKeySet();
private static volatile boolean userInfoInjectionPointDetected = false;
public Supplier setupTokenCache(OidcConfig config, Supplier vertx) {
return new Supplier() {
@Override
public DefaultTokenIntrospectionUserInfoCache get() {
return new DefaultTokenIntrospectionUserInfoCache(config, vertx.get());
}
};
}
public Supplier setup(OidcConfig config, Supplier vertx,
Supplier registrySupplier,
boolean userInfoInjectionPointDetected) {
OidcRecorder.userInfoInjectionPointDetected = userInfoInjectionPointDetected;
final Vertx vertxValue = vertx.get();
var defaultTlsConfiguration = registrySupplier.get().getDefault().orElse(null);
String defaultTenantId = config.defaultTenant.getTenantId().orElse(DEFAULT_TENANT_ID);
TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant,
!config.namedTenants.isEmpty(), defaultTenantId, defaultTlsConfiguration);
Map staticTenantsConfig = new HashMap<>();
for (Map.Entry tenant : config.namedTenants.entrySet()) {
OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), tenant.getValue().getTenantId());
staticTenantsConfig.put(tenant.getKey(),
createStaticTenantContext(vertxValue, tenant.getValue(), false, tenant.getKey(), defaultTlsConfiguration));
}
return new Supplier() {
@Override
public TenantConfigBean get() {
return new TenantConfigBean(staticTenantsConfig, dynamicTenantsConfig, defaultTenantContext,
new Function>() {
@Override
public Uni apply(OidcTenantConfig config) {
return createDynamicTenantContext(vertxValue, config, config.getTenantId().get(),
defaultTlsConfiguration);
}
});
}
};
}
private Uni createDynamicTenantContext(Vertx vertx,
OidcTenantConfig oidcConfig, String tenantId, TlsConfiguration defaultTlsConfiguration) {
if (oidcConfig.logout.backchannel.path.isPresent()) {
throw new ConfigurationException(
"BackChannel Logout is currently not supported for dynamic tenants");
}
if (!dynamicTenantsConfig.containsKey(tenantId)) {
Uni uniContext = createTenantContext(vertx, oidcConfig, false, tenantId,
defaultTlsConfiguration)
.onFailure().transform(new Function() {
@Override
public Throwable apply(Throwable t) {
return logTenantConfigContextFailure(t, tenantId);
}
});
return uniContext.onItem().transform(
new Function() {
@Override
public TenantConfigContext apply(TenantConfigContext t) {
dynamicTenantsConfig.putIfAbsent(tenantId, t);
return t;
}
});
} else {
return Uni.createFrom().item(dynamicTenantsConfig.get(tenantId));
}
}
private TenantConfigContext createStaticTenantContext(Vertx vertx,
OidcTenantConfig oidcConfig, boolean checkNamedTenants, String tenantId,
TlsConfiguration defaultTlsConfiguration) {
Uni uniContext = createTenantContext(vertx, oidcConfig, checkNamedTenants, tenantId,
defaultTlsConfiguration);
return uniContext.onFailure()
.recoverWithItem(new Function() {
@Override
public TenantConfigContext apply(Throwable t) {
if (t instanceof OIDCException) {
LOG.warnf("Tenant '%s': '%s'."
+ " OIDC server is not available yet, an attempt to connect will be made during the first request."
+ " Access to resources protected by this tenant may fail"
+ " if OIDC server will not become available",
tenantId, t.getMessage());
return new TenantConfigContext(null, oidcConfig, false);
}
logTenantConfigContextFailure(t, tenantId);
if (t instanceof ConfigurationException
&& !oidcConfig.authServerUrl.isPresent() && LaunchMode.DEVELOPMENT == LaunchMode.current()) {
// Let it start if it is a DEV mode and auth-server-url has not been configured yet
return new TenantConfigContext(null, oidcConfig, false);
}
// fail in all other cases
throw new OIDCException(t);
}
})
.await().atMost(oidcConfig.getConnectionTimeout());
}
private static Throwable logTenantConfigContextFailure(Throwable t, String tenantId) {
LOG.debugf(
"'%s' tenant is not initialized: '%s'. Access to resources protected by this tenant will fail.",
tenantId, t.getMessage());
return t;
}
@SuppressWarnings("resource")
private Uni createTenantContext(Vertx vertx, OidcTenantConfig oidcTenantConfig,
boolean checkNamedTenants, String tenantId, TlsConfiguration defaultTlsConfiguration) {
if (!oidcTenantConfig.tenantId.isPresent()) {
oidcTenantConfig.tenantId = Optional.of(tenantId);
}
final OidcTenantConfig oidcConfig = OidcUtils.resolveProviderConfig(oidcTenantConfig);
if (!oidcConfig.tenantEnabled) {
LOG.debugf("'%s' tenant configuration is disabled", tenantId);
return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig));
}
if (!oidcConfig.getAuthServerUrl().isPresent()) {
if (oidcConfig.getPublicKey().isPresent() && oidcConfig.certificateChain.trustStoreFile.isPresent()) {
throw new ConfigurationException("Both public key and certificate chain verification modes are enabled");
}
if (oidcConfig.getPublicKey().isPresent()) {
return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig));
}
if (oidcConfig.certificateChain.trustStoreFile.isPresent()) {
return Uni.createFrom().item(createTenantContextToVerifyCertChain(oidcConfig));
}
}
try {
if (!oidcConfig.getAuthServerUrl().isPresent()) {
if (DEFAULT_TENANT_ID.equals(oidcConfig.tenantId.get())) {
ArcContainer container = Arc.container();
if (container != null
&& (container.instance(TenantConfigResolver.class).isAvailable() || checkNamedTenants)) {
LOG.debugf("Default tenant is not configured and will be disabled"
+ " because either 'TenantConfigResolver' which will resolve tenant configurations is registered"
+ " or named tenants are configured.");
oidcConfig.setTenantEnabled(false);
return Uni.createFrom()
.item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig));
}
}
throw new ConfigurationException(
"'" + getConfigPropertyForTenant(tenantId, "auth-server-url") + "' property must be configured");
}
OidcCommonUtils.verifyEndpointUrl(oidcConfig.getAuthServerUrl().get());
OidcCommonUtils.verifyCommonConfiguration(oidcConfig, OidcUtils.isServiceApp(oidcConfig), true);
} catch (ConfigurationException t) {
return Uni.createFrom().failure(t);
}
if (oidcConfig.roles.source.orElse(null) == Source.userinfo && !enableUserInfo(oidcConfig)) {
throw new ConfigurationException(
"UserInfo is not required but UserInfo is expected to be the source of authorization roles");
}
if (oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false) && !OidcUtils.isWebApp(oidcConfig)
&& !enableUserInfo(oidcConfig)) {
throw new ConfigurationException(
"UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled");
}
if (!oidcConfig.authentication.isIdTokenRequired().orElse(true) && !enableUserInfo(oidcConfig)) {
throw new ConfigurationException(
"UserInfo is not required but it will be needed to verify a code flow access token");
}
if (!oidcConfig.discoveryEnabled.orElse(true)) {
if (!OidcUtils.isServiceApp(oidcConfig)) {
if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) {
String authorizationPathProperty = getConfigPropertyForTenant(tenantId, "authorization-path");
String tokenPathProperty = getConfigPropertyForTenant(tenantId, "token-path");
throw new ConfigurationException(
"'web-app' applications must have '" + authorizationPathProperty + "' and '" + tokenPathProperty
+ "' properties "
+ "set when the discovery is disabled.",
Set.of(authorizationPathProperty, tokenPathProperty));
}
}
// JWK and introspection endpoints have to be set for both 'web-app' and 'service' applications
if (!oidcConfig.jwksPath.isPresent() && !oidcConfig.introspectionPath.isPresent()) {
if (!oidcConfig.authentication.isIdTokenRequired().orElse(true)
&& oidcConfig.authentication.isUserInfoRequired().orElse(false)) {
LOG.debugf("tenant %s supports only UserInfo", oidcConfig.tenantId.get());
} else {
throw new ConfigurationException(
"Either 'jwks-path' or 'introspection-path' properties must be set when the discovery is disabled.",
Set.of("quarkus.oidc.jwks-path", "quarkus.oidc.introspection-path"));
}
}
if (oidcConfig.authentication.userInfoRequired.orElse(false) && !oidcConfig.userInfoPath.isPresent()) {
String configProperty = getConfigPropertyForTenant(tenantId, "user-info-path");
throw new ConfigurationException(
"UserInfo is required but '" + configProperty + "' is not configured.",
Set.of(configProperty));
}
}
if (OidcUtils.isServiceApp(oidcConfig)) {
if (oidcConfig.token.refreshExpired) {
throw new ConfigurationException(
"The '" + getConfigPropertyForTenant(tenantId, "token.refresh-expired")
+ "' property can only be enabled for " + ApplicationType.WEB_APP
+ " application types");
}
if (!oidcConfig.token.refreshTokenTimeSkew.isEmpty()) {
throw new ConfigurationException(
"The '" + getConfigPropertyForTenant(tenantId, "token.refresh-token-time-skew")
+ "' property can only be enabled for " + ApplicationType.WEB_APP
+ " application types");
}
if (oidcConfig.logout.path.isPresent()) {
throw new ConfigurationException(
"The '" + getConfigPropertyForTenant(tenantId, "logout.path") + "' property can only be enabled for "
+ ApplicationType.WEB_APP + " application types");
}
if (oidcConfig.roles.source.isPresent() && oidcConfig.roles.source.get() == Source.idtoken) {
throw new ConfigurationException(
"The '" + getConfigPropertyForTenant(tenantId, "roles.source")
+ "' property can only be set to 'idtoken' for " + ApplicationType.WEB_APP
+ " application types");
}
} else {
if (!oidcConfig.token.refreshTokenTimeSkew.isEmpty()) {
oidcConfig.token.setRefreshExpired(true);
}
}
if (oidcConfig.tokenStateManager.strategy != Strategy.KEEP_ALL_TOKENS) {
if (oidcConfig.authentication.isUserInfoRequired().orElse(false)
|| oidcConfig.roles.source.orElse(null) == Source.userinfo) {
throw new ConfigurationException(
"UserInfo is required but DefaultTokenStateManager is configured to not keep the access token");
}
if (oidcConfig.roles.source.orElse(null) == Source.accesstoken) {
throw new ConfigurationException(
"Access token is required to check the roles but DefaultTokenStateManager is configured to not keep the access token");
}
}
if (oidcConfig.token.verifyAccessTokenWithUserInfo.orElse(false)) {
if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
if (oidcConfig.userInfoPath.isEmpty()) {
throw new ConfigurationException(
"UserInfo path is missing but 'verifyAccessTokenWithUserInfo' is enabled");
}
if (oidcConfig.introspectionPath.isPresent()) {
throw new ConfigurationException(
"Introspection path is configured and 'verifyAccessTokenWithUserInfo' is enabled, these options are mutually exclusive");
}
}
}
if (!oidcConfig.token.isIssuedAtRequired() && oidcConfig.token.getAge().isPresent()) {
String tokenIssuedAtRequired = getConfigPropertyForTenant(tenantId, "token.issued-at-required");
String tokenAge = getConfigPropertyForTenant(tenantId, "token.age");
throw new ConfigurationException(
"The '" + tokenIssuedAtRequired + "' can only be set to false if '" + tokenAge + "' is not set." +
" Either set '" + tokenIssuedAtRequired + "' to true or do not set '" + tokenAge + "'.",
Set.of(tokenIssuedAtRequired, tokenAge));
}
return createOidcProvider(oidcConfig, vertx, defaultTlsConfiguration)
.onItem().transform(new Function() {
@Override
public TenantConfigContext apply(OidcProvider p) {
return new TenantConfigContext(p, oidcConfig);
}
});
}
private static String getConfigPropertyForTenant(String tenantId, String configSubKey) {
if (DEFAULT_TENANT_ID.equals(tenantId)) {
return "quarkus.oidc." + configSubKey;
} else {
return "quarkus.oidc." + tenantId + "." + configSubKey;
}
}
private static boolean enableUserInfo(OidcTenantConfig oidcConfig) {
Optional userInfoRequired = oidcConfig.authentication.isUserInfoRequired();
if (userInfoRequired.isPresent()) {
if (!userInfoRequired.get()) {
return false;
}
} else {
oidcConfig.authentication.setUserInfoRequired(true);
}
return true;
}
private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantConfig oidcConfig) {
if (!OidcUtils.isServiceApp(oidcConfig)) {
throw new ConfigurationException("'public-key' property can only be used with the 'service' applications");
}
LOG.debug("'public-key' property for the local token verification is set,"
+ " no connection to the OIDC server will be created");
return new TenantConfigContext(
new OidcProvider(oidcConfig.publicKey.get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig);
}
private static TenantConfigContext createTenantContextToVerifyCertChain(OidcTenantConfig oidcConfig) {
if (!OidcUtils.isServiceApp(oidcConfig)) {
throw new ConfigurationException(
"Currently only 'service' applications can be used to verify tokens with inlined certificate chains");
}
return new TenantConfigContext(
new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig);
}
public static Optional toProxyOptions(OidcCommonConfig.Proxy proxyConfig) {
return OidcCommonUtils.toProxyOptions(proxyConfig);
}
protected static OIDCException toOidcException(Throwable cause, String authServerUrl, String tenantId) {
final String message = OidcCommonUtils.formatConnectionErrorMessage(authServerUrl);
LOG.warn(message);
fireOidcServerNotAvailableEvent(authServerUrl, tenantId);
return new OIDCException("OIDC Server is not available", cause);
}
protected static Uni createOidcProvider(OidcTenantConfig oidcConfig, Vertx vertx,
TlsConfiguration defaultTlsConfiguration) {
return createOidcClientUni(oidcConfig, vertx, defaultTlsConfiguration)
.flatMap(new Function>() {
@Override
public Uni apply(OidcProviderClient client) {
if (oidcConfig.jwks.resolveEarly
&& client.getMetadata().getJsonWebKeySetUri() != null
&& !oidcConfig.token.requireJwtIntrospectionOnly) {
return getJsonWebSetUni(client, oidcConfig).onItem()
.transform(new Function() {
@Override
public OidcProvider apply(JsonWebKeySet jwks) {
return new OidcProvider(client, oidcConfig, jwks,
readTokenDecryptionKey(oidcConfig));
}
});
} else {
return Uni.createFrom()
.item(new OidcProvider(client, oidcConfig, null, readTokenDecryptionKey(oidcConfig)));
}
}
});
}
private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) {
if (oidcConfig.token.decryptionKeyLocation.isPresent()) {
try {
Key key = null;
String keyContent = KeyUtils.readKeyContent(oidcConfig.token.decryptionKeyLocation.get());
if (keyContent != null) {
List keys = KeyUtils.loadJsonWebKeys(keyContent);
if (keys != null && keys.size() == 1 &&
(keys.get(0).getAlgorithm() == null
|| keys.get(0).getAlgorithm().equals(KeyEncryptionAlgorithm.RSA_OAEP.getAlgorithm()))
&& ("enc".equals(keys.get(0).getUse()) || keys.get(0).getUse() == null)) {
key = PublicJsonWebKey.class.cast(keys.get(0)).getPrivateKey();
}
}
if (key == null) {
key = KeyUtils.decodeDecryptionPrivateKey(keyContent);
}
return key;
} catch (Exception ex) {
throw new ConfigurationException(
String.format("Token decryption key for tenant %s can not be read from %s",
oidcConfig.tenantId.get(), oidcConfig.token.decryptionKeyLocation.get()),
ex);
}
} else {
return null;
}
}
protected static Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) {
if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
String tenantId = oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID);
if (shouldFireOidcServerAvailableEvent(tenantId)) {
return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig)
.invoke(new Runnable() {
@Override
public void run() {
fireOidcServerAvailableEvent(oidcConfig.authServerUrl.get(), tenantId);
}
});
}
return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig);
} else {
return client.getJsonWebKeySet(null);
}
}
private static Uni getJsonWebSetUniWhenDiscoveryDisabled(OidcProviderClient client,
OidcTenantConfig oidcConfig) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
.retry()
.withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION)
.expireIn(connectionDelayInMillisecs)
.onFailure()
.transform(new Function() {
@Override
public Throwable apply(Throwable t) {
return toOidcException(t, oidcConfig.authServerUrl.get(),
oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID));
}
})
.onFailure()
.invoke(client::close);
}
protected static Uni createOidcClientUni(OidcTenantConfig oidcConfig, Vertx vertx,
TlsConfiguration defaultTlsConfiguration) {
String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig);
WebClientOptions options = new WebClientOptions();
OidcCommonUtils.setHttpClientOptions(oidcConfig, options, defaultTlsConfiguration);
var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx);
WebClient client = WebClient.create(mutinyVertx, options);
Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters();
Uni metadataUni = null;
if (!oidcConfig.discoveryEnabled.orElse(true)) {
metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString));
} else {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
OidcRequestContextProperties contextProps = new OidcRequestContextProperties(
Map.of(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID)));
metadataUni = OidcCommonUtils
.discoverMetadata(client, oidcRequestFilters, contextProps, authServerUriString, connectionDelayInMillisecs,
mutinyVertx,
oidcConfig.useBlockingDnsLookup)
.onItem()
.transform(new Function() {
@Override
public OidcConfigurationMetadata apply(JsonObject json) {
return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString),
OidcCommonUtils.getDiscoveryUri(authServerUriString));
}
});
}
return metadataUni.onItemOrFailure()
.transformToUni(new BiFunction>() {
@Override
public Uni apply(OidcConfigurationMetadata metadata, Throwable t) {
String tenantId = oidcConfig.tenantId.orElse(DEFAULT_TENANT_ID);
if (t != null) {
client.close();
return Uni.createFrom().failure(toOidcException(t, authServerUriString, tenantId));
}
if (shouldFireOidcServerAvailableEvent(tenantId)) {
fireOidcServerAvailableEvent(authServerUriString, tenantId);
}
if (metadata == null) {
client.close();
return Uni.createFrom().failure(new ConfigurationException(
"OpenId Connect Provider configuration metadata is not configured and can not be discovered"));
}
if (oidcConfig.logout.path.isPresent()) {
if (!oidcConfig.endSessionPath.isPresent() && metadata.getEndSessionUri() == null) {
client.close();
return Uni.createFrom().failure(new ConfigurationException(
"The application supports RP-Initiated Logout but the OpenID Provider does not advertise the end_session_endpoint"));
}
}
if (userInfoInjectionPointDetected && metadata.getUserInfoUri() != null) {
enableUserInfo(oidcConfig);
}
if (oidcConfig.authentication.userInfoRequired.orElse(false) && metadata.getUserInfoUri() == null) {
client.close();
return Uni.createFrom().failure(new ConfigurationException(
"UserInfo is required but the OpenID Provider UserInfo endpoint is not configured."
+ " Use 'quarkus.oidc.user-info-path' if the discovery is disabled."));
}
return Uni.createFrom()
.item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters));
}
});
}
private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oidcConfig, String authServerUriString) {
String tokenUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath);
String introspectionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString,
oidcConfig.introspectionPath);
String authorizationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString,
oidcConfig.authorizationPath);
String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath);
String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath);
String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath);
return new OidcConfigurationMetadata(tokenUri,
introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri,
oidcConfig.token.issuer.orElse(null));
}
private static void fireOidcServerNotAvailableEvent(String authServerUrl, String tenantId) {
if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_NOT_AVAILABLE)) {
tenantsExpectingServerAvailableEvents.add(tenantId);
}
}
private static void fireOidcServerAvailableEvent(String authServerUrl, String tenantId) {
if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_AVAILABLE)) {
tenantsExpectingServerAvailableEvents.remove(tenantId);
}
}
private static boolean shouldFireOidcServerAvailableEvent(String tenantId) {
return tenantsExpectingServerAvailableEvents.contains(tenantId);
}
private static boolean fireOidcServerEvent(String authServerUrl, SecurityEvent.Type eventType) {
if (ConfigProvider.getConfig().getOptionalValue(SECURITY_EVENTS_ENABLED_CONFIG_KEY, boolean.class).orElse(true)) {
SecurityEventHelper.fire(
Arc.container().beanManager().getEvent().select(SecurityEvent.class),
new SecurityEvent(eventType, Map.of(AUTH_SERVER_URL, authServerUrl)));
return true;
}
return false;
}
public Function> tenantResolverInterceptorCreator() {
return new Function>() {
@Override
public Consumer apply(String tenantId) {
return new Consumer() {
@Override
public void accept(RoutingContext routingContext) {
OidcTenantConfig tenantConfig = routingContext.get(OidcTenantConfig.class.getName());
if (tenantConfig != null) {
// authentication has happened before @Tenant annotation was matched with the HTTP request
String tenantUsedForAuth = tenantConfig.tenantId.orElse(null);
if (tenantId.equals(tenantUsedForAuth)) {
// @Tenant selects the same tenant as already selected
return;
} else {
// @Tenant selects the different tenant than already selected
throw new AuthenticationFailedException(
"""
The '%1$s' selected with the @Tenant annotation must be used to authenticate
the request but it was already authenticated with the '%2$s' tenant. It
can happen if the '%1$s' is selected with an annotation but '%2$s' is
resolved during authentication required by the HTTP Security Policy which
is enforced before the JAX-RS chain is run. In such cases, please set the
'quarkus.http.auth.permission."permissions".applies-to=JAXRS' to all HTTP
Security Policies which secure the same REST endpoints as the ones
where the '%1$s' tenant is resolved by the '@Tenant' annotation.
"""
.formatted(tenantId, tenantUsedForAuth));
}
}
LOG.debugf("@Tenant annotation set a '%s' tenant id on the %s request path", tenantId,
routingContext.request().path());
routingContext.put(OidcUtils.TENANT_ID_SET_BY_ANNOTATION, tenantId);
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
}
};
}
};
}
public Supplier createTenantIdentityProvider(String tenantName) {
return new Supplier() {
@Override
public TenantIdentityProvider get() {
return new TenantSpecificOidcIdentityProvider(tenantName);
}
};
}
private static final class TenantSpecificOidcIdentityProvider extends OidcIdentityProvider
implements TenantIdentityProvider {
private final String tenantId;
private final BlockingSecurityExecutor blockingExecutor;
private TenantSpecificOidcIdentityProvider(String tenantId) {
super(Arc.container().instance(DefaultTenantConfigResolver.class).get(),
Arc.container().instance(BlockingSecurityExecutor.class).get());
this.blockingExecutor = Arc.container().instance(BlockingSecurityExecutor.class).get();
if (tenantId.equals(DEFAULT_TENANT_ID)) {
OidcConfig config = Arc.container().instance(OidcConfig.class).get();
this.tenantId = config.defaultTenant.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID);
} else {
this.tenantId = tenantId;
}
}
@Override
public Uni authenticate(AccessTokenCredential token) {
return authenticate(new TokenAuthenticationRequest(token));
}
@Override
protected Uni resolveTenantConfigContext(TokenAuthenticationRequest request,
AuthenticationRequestContext context) {
return tenantResolver.resolveContext(tenantId).onItem().ifNull().failWith(new Supplier() {
@Override
public Throwable get() {
return new OIDCException("Failed to resolve tenant context");
}
});
}
@Override
protected Map getRequestData(TokenAuthenticationRequest request) {
RoutingContext context = getRoutingContextAttribute(request);
if (context != null) {
return context.data();
}
return new HashMap<>();
}
private Uni authenticate(TokenAuthenticationRequest request) {
return authenticate(request, new AuthenticationRequestContext() {
@Override
public Uni runBlocking(Supplier function) {
return blockingExecutor.executeBlocking(function);
}
});
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy