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

Add support for well-known OIDC providers #22572

Merged
merged 1 commit into from
Jan 18, 2022
Merged
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
Expand Up @@ -666,37 +666,24 @@ If the `OAuth2` provider supports the introspection endpoint then you may be abl

Configuring the endpoint to request <<user-info,UserInfo>> is the only way `quarkus-oidc` can be integrated with the providers such as `GitHib`.

Note that requiring <<user-info,UserInfo>> involves making a remote call on every request - therefore you may want tp consider caching `UserInfo` data, see <<token-introspection-userinfo-cache,Token Introspection and UserInfo Cache> for more details.
Note that requiring <<user-info,UserInfo>> involves making a remote call on every request - therefore you may want to consider caching `UserInfo` data, see <<token-introspection-userinfo-cache,Token Introspection and UserInfo Cache> for more details.

Also, OAuth2 servers may not support a well-known configuration endpoint in which case the discovery has to be disabled and the authorization, token, and introspection and/or userinfo endpoint paths have to be configured manually.

Here is how you can integrate `quarkus-oidc` with `GitHub` after you have link:https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app[created a GitHub OAuth application]. Configure your Quarkus endpoint like this:

[source,properties]
----
quarkus.oidc.auth-server-url=https://github.com/login/oauth
quarkus.oidc.discovery-enabled=false
quarkus.oidc.authorization-path=authorize
quarkus.oidc.token-path=access_token
quarkus.oidc.user-info-path=https://api.github.com/user

# See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
quarkus.oidc.authentication.scopes=user:email
quarkus.oidc.provider=github
quarkus.oidc.client-id=github_app_clientid
quarkus.oidc.credentials.secret=github_app_clientsecret

# Make sure a user info is required
quarkus.oidc.authentication.user-info-required=true
# user:email scope is requested by default, use 'quarkus.oidc.authentication.scopes' to request differrent scopes such as `read:user`.
# See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps for more information.

# Consider enabling UserInfo Cache
# quarkus.oidc.token-cache.max-size=1000
# quarkus.oidc.token-cache.time-to-live=5M

# Allow the code flow responses without ID tokens
quarkus.oidc.authentication.id-token-required=false

quarkus.oidc.application-type=web-app

quarkus.oidc.client-id=github_app_clientid
quarkus.oidc.credentials.secret=github_app_clientsecret
----

This is all what is needed for an endpoint like this one to return the currently authenticated user's profile with `GET http://localhost:8080/github/userinfo` and access it as the individual `UserInfo` properties:
Expand Down Expand Up @@ -831,8 +818,7 @@ and configure Google OIDC properties:

[source, properties]
----
quarkus.oidc.auth-server-url=https://accounts.google.com
quarkus.oidc.application-type=web-app
quarkus.oidc.provider=google
quarkus.oidc.client-id={GOOGLE_CLIENT_ID}
quarkus.oidc.credentials.secret={GOOGLE_CLIENT_SECRET}
quarkus.oidc.token.issuer=https://accounts.google.com
Expand Down Expand Up @@ -1001,12 +987,14 @@ Apple OpenID Connect Provider uses a `client_secret_post` method where a secret

[source,properties]
----
quarkus.oidc.auth-server-url=${apple.url}
quarkus.oidc.client-id=${apple.client-id}
quarkus.oidc.credentials.client-secret.method=post-jwt
# Apple provider configuration sets a 'client_secret_post_jwt' authentication method
quarkus.oidc.provider=apple

quarkus.oidc.client-id=${apple.client-id}
quarkus.oidc.credentials.jwt.key-file=ecPrivateKey.pem
quarkus.oidc.credentials.jwt.signature-algorithm=ES256
quarkus.oidc.credentials.jwt.token-key-id=${apple.key-id}
# Apple provider configuration sets ES256 signature algorithm

quarkus.oidc.credentials.jwt.subject=${apple.subject}
quarkus.oidc.credentials.jwt.issuer=${apple.issuer}
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerTenantConfig.KeycloakConfigPolicyEnforcer.PathCacheConfig;
import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Roles.Source;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Tls.Verification;
import io.quarkus.oidc.runtime.OidcConfig;
Expand Down Expand Up @@ -55,7 +56,7 @@ private static PolicyEnforcer createPolicyEnforcer(OidcTenantConfig oidcConfig,
KeycloakPolicyEnforcerTenantConfig keycloakPolicyEnforcerConfig,
TlsConfig tlsConfig) {

if (oidcConfig.applicationType == OidcTenantConfig.ApplicationType.WEB_APP
if (oidcConfig.applicationType.orElse(ApplicationType.SERVICE) == OidcTenantConfig.ApplicationType.WEB_APP
&& oidcConfig.roles.source.orElse(null) != Source.accesstoken) {
throw new OIDCException("Application 'web-app' type is only supported if access token is the source of roles");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ protected static Uni<OidcClient> createOidcClientUni(OidcClientConfig oidcConfig
tokenRequestUriUni = Uni.createFrom().item(oidcConfig.tokenPath.get());
} else {
String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig);
if (!oidcConfig.discoveryEnabled) {
if (!oidcConfig.discoveryEnabled.orElse(true)) {
tokenRequestUriUni = Uni.createFrom()
.item(OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ public class OidcCommonConfig {
* Enables OIDC discovery.
* If the discovery is disabled then the OIDC endpoint URLs must be configured individually.
*/
@ConfigItem(defaultValue = "true")
public boolean discoveryEnabled = true;
@ConfigItem(defaultValueDocumentation = "true")
public Optional<Boolean> discoveryEnabled = Optional.empty();

/**
* Relative path or absolute URL of the OIDC token endpoint which issues access and refresh tokens.
Expand Down Expand Up @@ -549,12 +549,12 @@ public void setCredentials(Credentials credentials) {
this.credentials = credentials;
}

public boolean isDiscoveryEnabled() {
public Optional<Boolean> isDiscoveryEnabled() {
return discoveryEnabled;
}

public void setDiscoveryEnabled(boolean enabled) {
this.discoveryEnabled = enabled;
this.discoveryEnabled = Optional.of(enabled);
}

public Proxy getProxy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<
config.setAuthServerUrl(getIssuerUrl() + "/realms/quarkus");
config.setClientId("quarkus-web-app");
config.getCredentials().setSecret("secret");
config.applicationType = ApplicationType.WEB_APP;
config.setApplicationType(ApplicationType.WEB_APP);
return Uni.createFrom().item(config);
}
return Uni.createFrom().nullItem();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public class OidcTenantConfig extends OidcCommonConfig {
/**
* The application type, which can be one of the following values from enum {@link ApplicationType}.
*/
@ConfigItem(defaultValue = "service")
public ApplicationType applicationType = ApplicationType.SERVICE;
@ConfigItem(defaultValueDocumentation = "service")
public Optional<ApplicationType> applicationType = Optional.empty();

/**
* Relative path or absolute URL of the OIDC authorization endpoint which authenticates the users.
Expand Down Expand Up @@ -535,10 +535,10 @@ public static class Authentication {
public Optional<String> cookieDomain = Optional.empty();

/**
* If this property is set to 'true' then an OIDC UserInfo endpoint will be called
* If this property is set to 'true' then an OIDC UserInfo endpoint will be called.
*/
@ConfigItem(defaultValue = "false")
public boolean userInfoRequired;
@ConfigItem(defaultValueDocumentation = "false")
public Optional<Boolean> userInfoRequired = Optional.empty();
sberyozkin marked this conversation as resolved.
Show resolved Hide resolved

/**
* Session age extension in minutes.
Expand All @@ -564,11 +564,12 @@ public static class Authentication {
public boolean javaScriptAutoRedirect = true;

/**
* Requires that ID token is available when the authorization code flow completes. In most case this property
* should be enabled. Disable this property only when you need to use the authorization code flow with OAuth2 providers.
* Requires that ID token is available when the authorization code flow completes.
* Disable this property only when you need to use the authorization code flow with OAuth2 providers which do not return
* ID token.
*/
@ConfigItem(defaultValue = "true")
public boolean idTokenRequired = true;
@ConfigItem(defaultValueDocumentation = "true")
public Optional<Boolean> idTokenRequired = Optional.empty();

public boolean isJavaScriptAutoRedirect() {
return javaScriptAutoRedirect;
Expand All @@ -590,8 +591,8 @@ public Optional<List<String>> getScopes() {
return scopes;
}

public void setScopes(Optional<List<String>> scopes) {
this.scopes = scopes;
public void setScopes(List<String> scopes) {
this.scopes = Optional.of(scopes);
}

public Map<String, String> getExtraParams() {
Expand Down Expand Up @@ -642,12 +643,12 @@ public void setCookieDomain(String cookieDomain) {
this.cookieDomain = Optional.of(cookieDomain);
}

public boolean isUserInfoRequired() {
public Optional<Boolean> isUserInfoRequired() {
return userInfoRequired;
}

public void setUserInfoRequired(boolean userInfoRequired) {
this.userInfoRequired = userInfoRequired;
this.userInfoRequired = Optional.of(userInfoRequired);
}

public boolean isRemoveRedirectParameters() {
Expand Down Expand Up @@ -682,12 +683,12 @@ public void setCookiePathHeader(String cookiePathHeader) {
this.cookiePathHeader = Optional.of(cookiePathHeader);
}

public boolean isIdTokenRequired() {
public Optional<Boolean> isIdTokenRequired() {
return idTokenRequired;
}

public void setIdTokenRequired(boolean idTokenRequired) {
this.idTokenRequired = idTokenRequired;
this.idTokenRequired = Optional.of(idTokenRequired);
}

public Optional<String> getCookieSuffix() {
Expand Down Expand Up @@ -928,12 +929,34 @@ public static enum ApplicationType {
HYBRID
}

public ApplicationType getApplicationType() {
/**
* Well known OpenId Connect provider identifier
*/
@ConfigItem
public Optional<Provider> provider = Optional.empty();

public static enum Provider {
APPLE,
FACEBOOK,
GITHUB,
GOOGLE,
MICROSOFT
}

public Optional<Provider> getProvider() {
return provider;
}

public void setProvider(Provider provider) {
this.provider = Optional.of(provider);
}

public Optional<ApplicationType> getApplicationType() {
return applicationType;
}

public void setApplicationType(ApplicationType type) {
this.applicationType = type;
this.applicationType = Optional.of(type);
}

public boolean isAllowTokenIntrospectionCache() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public Uni<? extends SecurityIdentity> apply(AuthorizationCodeTokens session) {
context.put(AuthorizationCodeTokens.class.getName(), session);
return authenticate(identityProviderManager, context,
new IdTokenCredential(session.getIdToken(),
!configContext.oidcConfig.authentication.isIdTokenRequired()))
!configContext.oidcConfig.authentication.isIdTokenRequired().orElse(true)))
.call(new Function<SecurityIdentity, Uni<?>>() {
@Override
public Uni<Void> apply(SecurityIdentity identity) {
Expand Down Expand Up @@ -294,7 +294,7 @@ public Uni<SecurityIdentity> apply(final AuthorizationCodeTokens tokens, final T

boolean internalIdToken = false;
if (tokens.getIdToken() == null) {
if (configContext.oidcConfig.authentication.isIdTokenRequired()) {
if (configContext.oidcConfig.authentication.isIdTokenRequired().orElse(true)) {
return Uni.createFrom()
.failure(new AuthenticationCompletionException("ID Token is not available"));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ private Uni<OidcTenantConfig> getDynamicTenantConfig(RoutingContext context) {
if (oidcConfig == null) {
//shouldn't happen, but guard against it anyway
oidcConfig = Uni.createFrom().nullItem();
} else {
oidcConfig = oidcConfig.onItem().transform(cfg -> OidcUtils.resolveProviderConfig(cfg));
}
context.put(CURRENT_DYNAMIC_TENANT_CONFIG, oidcConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public int getCacheSize() {

public void clearCache() {
cacheMap.clear();
size.set(0);
}

private void removeInvalidEntries() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.quarkus.oidc.OIDCException;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
Expand Down Expand Up @@ -77,10 +78,11 @@ public OidcTenantConfig apply(OidcTenantConfig oidcTenantConfig) {
}

private boolean isWebApp(RoutingContext context, OidcTenantConfig oidcConfig) {
if (OidcTenantConfig.ApplicationType.HYBRID == oidcConfig.applicationType) {
ApplicationType applicationType = oidcConfig.applicationType.orElse(ApplicationType.SERVICE);
if (OidcTenantConfig.ApplicationType.HYBRID == applicationType) {
return context.request().getHeader("Authorization") == null;
}
return OidcTenantConfig.ApplicationType.WEB_APP == oidcConfig.applicationType;
return OidcTenantConfig.ApplicationType.WEB_APP == applicationType;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private Uni<SecurityIdentity> validateTokenWithOidcServer(RoutingContext vertxCo
vertxContext.put(CODE_ACCESS_TOKEN_RESULT, codeAccessTokenResult);
}

Uni<UserInfo> userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired()
Uni<UserInfo> userInfo = resolvedContext.oidcConfig.authentication.isUserInfoRequired().orElse(false)
? getUserInfoUni(vertxContext, request, resolvedContext)
: NULL_USER_INFO_UNI;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,14 @@ private static Throwable logTenantConfigContextFailure(Throwable t, String tenan
}

@SuppressWarnings("resource")
private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConfig oidcConfig, TlsConfig tlsConfig,
private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConfig oidcTenantConfig, TlsConfig tlsConfig,
String tenantId) {
if (!oidcConfig.tenantId.isPresent()) {
oidcConfig.tenantId = Optional.of(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), oidcConfig));
Expand All @@ -148,7 +151,7 @@ private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConf
return Uni.createFrom().failure(t);
}

if (!oidcConfig.discoveryEnabled) {
if (!oidcConfig.discoveryEnabled.orElse(true)) {
if (!isServiceApp(oidcConfig)) {
if (!oidcConfig.authorizationPath.isPresent() || !oidcConfig.tokenPath.isPresent()) {
throw new ConfigurationException(
Expand All @@ -159,7 +162,8 @@ private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConf
}
// 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() && oidcConfig.authentication.isUserInfoRequired()) {
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(
Expand Down Expand Up @@ -189,7 +193,8 @@ private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConf

if (oidcConfig.tokenStateManager.strategy != Strategy.KEEP_ALL_TOKENS) {

if (oidcConfig.authentication.isUserInfoRequired() || oidcConfig.roles.source.orElse(null) == Source.userinfo) {
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");
}
Expand Down Expand Up @@ -250,7 +255,7 @@ public OidcProvider apply(JsonWebKeySet jwks) {
}

protected static Uni<JsonWebKeySet> getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) {
if (!oidcConfig.isDiscoveryEnabled()) {
if (!oidcConfig.isDiscoveryEnabled().orElse(true)) {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
return client.getJsonWebKeySet().onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
.retry()
Expand All @@ -277,7 +282,7 @@ protected static Uni<OidcProviderClient> createOidcClientUni(OidcTenantConfig oi
WebClient client = WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options);

Uni<OidcConfigurationMetadata> metadataUni = null;
if (!oidcConfig.discoveryEnabled) {
if (!oidcConfig.discoveryEnabled.orElse(true)) {
metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString));
} else {
final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
Expand Down Expand Up @@ -327,7 +332,7 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi
}

private static boolean isServiceApp(OidcTenantConfig oidcConfig) {
return ApplicationType.SERVICE.equals(oidcConfig.applicationType);
return ApplicationType.SERVICE.equals(oidcConfig.applicationType.orElse(ApplicationType.SERVICE));
}

private static void verifyAuthServerUrl(OidcCommonConfig oidcConfig) {
Expand Down
Loading