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 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.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.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.TenantConfigResolver;
import io.quarkus.oidc.common.runtime.OidcCommonConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.spi.runtime.MethodDescription;
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 Map dynamicTenantsConfig = new ConcurrentHashMap<>();
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, TlsConfig tlsConfig) {
final Vertx vertxValue = vertx.get();
String defaultTenantId = config.defaultTenant.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID);
TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant,
!config.namedTenants.isEmpty(), tlsConfig, defaultTenantId);
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, tlsConfig, tenant.getKey()));
}
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, tlsConfig, config.getTenantId().get());
}
});
}
};
}
public RuntimeValue methodInfoToDescription(String className, String methodName, String[] paramTypes) {
return new RuntimeValue<>(new MethodDescription(className, methodName, paramTypes));
}
private Uni createDynamicTenantContext(Vertx vertx,
OidcTenantConfig oidcConfig, TlsConfig tlsConfig, String tenantId) {
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, tlsConfig, tenantId);
uniContext.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, TlsConfig tlsConfig, String tenantId) {
Uni uniContext = createTenantContext(vertx, oidcConfig, checkNamedTenants, tlsConfig, tenantId);
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,
TlsConfig tlsConfig, String tenantId) {
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.getPublicKey().isPresent()) {
return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig));
}
try {
if (!oidcConfig.getAuthServerUrl().isPresent()) {
if (OidcUtils.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("'quarkus.oidc.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()) {
throw new ConfigurationException(
"'web-app' applications must have 'authorization-path' and 'token-path' properties "
+ "set when the discovery is disabled.",
Set.of("quarkus.oidc.authorization-path", "quarkus.oidc.token-path"));
}
}
// 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()) {
throw new ConfigurationException(
"UserInfo is required but 'quarkus.oidc.user-info-path' is not configured.",
Set.of("quarkus.oidc.user-info-path"));
}
}
if (OidcUtils.isServiceApp(oidcConfig)) {
if (oidcConfig.token.refreshExpired) {
throw new ConfigurationException(
"The 'token.refresh-expired' property can only be enabled for " + ApplicationType.WEB_APP
+ " application types");
}
if (!oidcConfig.token.refreshTokenTimeSkew.isEmpty()) {
throw new ConfigurationException(
"The '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 '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 '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");
}
}
}
return createOidcProvider(oidcConfig, tlsConfig, vertx)
.onItem().transform(new Function() {
@Override
public TenantConfigContext apply(OidcProvider p) {
return new TenantConfigContext(p, oidcConfig);
}
});
}
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);
}
public void setSecurityEventObserved(boolean isSecurityEventObserved) {
DefaultTenantConfigResolver bean = Arc.container().instance(DefaultTenantConfigResolver.class).get();
bean.setSecurityEventObserved(isSecurityEventObserved);
}
public static Optional toProxyOptions(OidcCommonConfig.Proxy proxyConfig) {
return OidcCommonUtils.toProxyOptions(proxyConfig);
}
protected static OIDCException toOidcException(Throwable cause, String authServerUrl) {
final String message = OidcCommonUtils.formatConnectionErrorMessage(authServerUrl);
LOG.warn(message);
return new OIDCException("OIDC Server is not available", cause);
}
protected static Uni createOidcProvider(OidcTenantConfig oidcConfig, TlsConfig tlsConfig, Vertx vertx) {
return createOidcClientUni(oidcConfig, tlsConfig, vertx).onItem()
.transformToUni(new Function>() {
@Override
public Uni apply(OidcProviderClient client) {
if (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)) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return client.getJsonWebKeySet().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());
}
})
.onFailure()
.invoke(client::close);
} else {
return client.getJsonWebKeySet();
}
}
protected static Uni createOidcClientUni(OidcTenantConfig oidcConfig,
TlsConfig tlsConfig, Vertx vertx) {
String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig);
WebClientOptions options = new WebClientOptions();
OidcCommonUtils.setHttpClientOptions(oidcConfig, tlsConfig, options);
WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options);
Uni metadataUni = null;
if (!oidcConfig.discoveryEnabled.orElse(true)) {
metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString));
} else {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
metadataUni = OidcCommonUtils.discoverMetadata(client, authServerUriString, connectionDelayInMillisecs)
.onItem()
.transform(new Function() {
@Override
public OidcConfigurationMetadata apply(JsonObject json) {
return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString));
}
});
}
return metadataUni.onItemOrFailure()
.transformToUni(new BiFunction>() {
@Override
public Uni apply(OidcConfigurationMetadata metadata, Throwable t) {
if (t != null) {
client.close();
return Uni.createFrom().failure(toOidcException(t, authServerUriString));
}
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 (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, metadata, oidcConfig));
}
});
}
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));
}
public Consumer createTenantResolverInterceptor(String tenantId) {
return new Consumer() {
@Override
public void accept(RoutingContext routingContext) {
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
}
};
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy