Skip to content

Commit

Permalink
Add support for well-known OIDC providers
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Jan 1, 2022
1 parent 5c120b1 commit 9eb0a29
Show file tree
Hide file tree
Showing 21 changed files with 301 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -674,29 +674,16 @@ Here is how you can integrate `quarkus-oidc` with `GitHub` after you have link:h

[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.
# 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
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
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
public Optional<ApplicationType> applicationType = Optional.empty();

/**
* Relative path or absolute URL of the OIDC authorization endpoint which authenticates the users.
Expand Down Expand Up @@ -537,8 +537,7 @@ public static class Authentication {
/**
* If this property is set to 'true' then an OIDC UserInfo endpoint will be called
*/
@ConfigItem(defaultValue = "false")
public boolean userInfoRequired;
public Optional<Boolean> userInfoRequired = Optional.empty();

/**
* Session age extension in minutes.
Expand Down Expand Up @@ -567,8 +566,8 @@ public static class Authentication {
* 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.
*/
@ConfigItem(defaultValue = "true")
public boolean idTokenRequired = true;
@ConfigItem
public Optional<Boolean> idTokenRequired = Optional.empty();

public boolean isJavaScriptAutoRedirect() {
return javaScriptAutoRedirect;
Expand All @@ -590,8 +589,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 +641,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 +681,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 +927,12 @@ public static enum ApplicationType {
HYBRID
}

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

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

public boolean isAllowTokenIntrospectionCache() {
Expand All @@ -951,4 +950,25 @@ public boolean isAllowUserInfoCache() {
public void setAllowUserInfoCache(boolean allowUserInfoCache) {
this.allowUserInfoCache = allowUserInfoCache;
}

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

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

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

public enum Provider {
/**
* GitHub
*/
GITHUB
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,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 @@ -289,7 +289,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 @@ -10,6 +10,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 @@ -80,10 +81,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

0 comments on commit 9eb0a29

Please sign in to comment.