Skip to content

Commit

Permalink
Merge pull request #6348 from pedroigor/issue-4448
Browse files Browse the repository at this point in the history
[fixes #4448] - OIDC Multi-tenancy Support
  • Loading branch information
sberyozkin authored Jan 15, 2020
2 parents b266c4b + d22d38e commit 92ab536
Show file tree
Hide file tree
Showing 29 changed files with 1,213 additions and 327 deletions.
1 change: 1 addition & 0 deletions ci-templates/stages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ stages:
- elytron-resteasy
- oidc
- oidc-code-flow
- oidc-tenancy
- vault-app
- keycloak-authorization
name: security_2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;

import io.quarkus.oidc.runtime.OidcConfig;
import io.quarkus.oidc.runtime.OidcTenantConfig;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
Expand Down Expand Up @@ -87,7 +88,7 @@ public CompletionStage<Boolean> apply(Permission permission) {

public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) {
AdapterConfig adapterConfig = new AdapterConfig();
String authServerUrl = oidcConfig.getAuthServerUrl();
String authServerUrl = oidcConfig.defaultTenant.getAuthServerUrl().get();

try {
adapterConfig.setRealm(authServerUrl.substring(authServerUrl.lastIndexOf('/') + 1));
Expand All @@ -96,8 +97,8 @@ public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) {
throw new RuntimeException("Failed to parse the realm name.", cause);
}

adapterConfig.setResource(oidcConfig.getClientId().get());
adapterConfig.setCredentials(getCredentials(oidcConfig));
adapterConfig.setResource(oidcConfig.defaultTenant.getClientId().get());
adapterConfig.setCredentials(getCredentials(oidcConfig.defaultTenant));

PolicyEnforcerConfig enforcerConfig = getPolicyEnforcerConfig(config, adapterConfig);

Expand All @@ -111,7 +112,7 @@ public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) {
new PolicyEnforcer(KeycloakDeploymentBuilder.build(adapterConfig), adapterConfig));
}

private Map<String, Object> getCredentials(OidcConfig oidcConfig) {
private Map<String, Object> getCredentials(OidcTenantConfig oidcConfig) {
Map<String, Object> credentials = new HashMap<>();
Optional<String> clientSecret = oidcConfig.getCredentials().getSecret();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.oidc.runtime.BearerAuthenticationMechanism;
import io.quarkus.oidc.runtime.CodeAuthenticationMechanism;
import io.quarkus.oidc.runtime.DefaultTenantConfigResolver;
import io.quarkus.oidc.runtime.OidcBuildTimeConfig;
import io.quarkus.oidc.runtime.OidcConfig;
import io.quarkus.oidc.runtime.OidcIdentityProvider;
Expand Down Expand Up @@ -59,7 +60,8 @@ public AdditionalBeanBuildItem beans() {
}
return beans.addBeanClass(OidcJsonWebTokenProducer.class)
.addBeanClass(OidcTokenCredentialProducer.class)
.addBeanClass(OidcIdentityProvider.class).build();
.addBeanClass(OidcIdentityProvider.class)
.addBeanClass(DefaultTenantConfigResolver.class).build();
}

@BuildStep(onlyIf = IsEnabled.class)
Expand All @@ -71,7 +73,7 @@ EnableAllSecurityServicesBuildItem security() {
@BuildStep(onlyIf = IsEnabled.class)
public void setup(OidcConfig config, OidcRecorder recorder, InternalWebVertxBuildItem vertxBuildItem,
BeanContainerBuildItem bc) {
recorder.setup(config, buildTimeConfig, vertxBuildItem.getVertx(), bc.getValue());
recorder.setup(config, vertxBuildItem.getVertx(), bc.getValue());
}

static class IsEnabled implements BooleanSupplier {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package io.quarkus.oidc;

import io.quarkus.security.credential.TokenCredential;
import io.quarkus.oidc.runtime.ContextAwareTokenCredential;
import io.vertx.ext.web.RoutingContext;

public class AccessTokenCredential extends TokenCredential {
public class AccessTokenCredential extends ContextAwareTokenCredential {

private RefreshToken refreshToken;

public AccessTokenCredential() {
this(null);
this(null, null);
}

/**
* Create AccessTokenCredential
*
* @param accessToken - access token
*/
public AccessTokenCredential(String accessToken) {
this(accessToken, null);
public AccessTokenCredential(String accessToken, RoutingContext context) {
super(accessToken, "bearer", context);
}

/**
Expand All @@ -25,8 +26,8 @@ public AccessTokenCredential(String accessToken) {
* @param accessToken - access token
* @param refreshToken - refresh token which can be used to refresh this access token, may be null
*/
public AccessTokenCredential(String accessToken, RefreshToken refreshToken) {
super(accessToken, "bearer");
public AccessTokenCredential(String accessToken, RefreshToken refreshToken, RoutingContext context) {
this(accessToken, context);
this.refreshToken = refreshToken;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package io.quarkus.oidc;

import io.quarkus.security.credential.TokenCredential;
import io.quarkus.oidc.runtime.ContextAwareTokenCredential;
import io.vertx.ext.web.RoutingContext;

public class IdTokenCredential extends ContextAwareTokenCredential {

public class IdTokenCredential extends TokenCredential {
public IdTokenCredential() {
this(null);
this(null, null);
}

public IdTokenCredential(String token) {
super(token, "id_token");
public IdTokenCredential(String token, RoutingContext context) {
super(token, "id_token", context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.oidc;

import io.quarkus.oidc.runtime.OidcTenantConfig;
import io.vertx.ext.web.RoutingContext;

/**
* <p>
* A tenant resolver is responsible for resolving the {@link OidcTenantConfig} for tenants, dynamically.
*
* <p>
* Instead of implementing a {@link TenantResolver} that maps the tenant configuration based on an identifier and its
* corresponding entry in the application configuration file, beans implementing this interface can dynamically construct the
* tenant configuration without having to define each tenant in the application configuration file.
*/
public interface TenantConfigResolver {

/**
* Returns a {@link OidcTenantConfig} given a {@code RoutingContext}.
*
* @param context the routing context
* @return the tenant configuration. If {@code null}, indicates that the default configuration/tenant should be chosen
*/
OidcTenantConfig resolve(RoutingContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.oidc;

import io.vertx.ext.web.RoutingContext;

/**
* A tenant resolver is responsible for resolving tenants dynamically so that the proper configuration can be used accordingly.
*/
public interface TenantResolver {

/**
* Returns a tenant identifier given a {@code RoutingContext}, where the identifier will be used to choose the proper
* configuration during runtime.
*
* @param context the routing context
* @return the tenant identifier. If {@code null}, indicates that the default configuration/tenant should be chosen
*/
String resolve(RoutingContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,20 @@

import java.util.concurrent.CompletionStage;

import javax.inject.Inject;

import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.vertx.ext.auth.oauth2.OAuth2Auth;

abstract class AbstractOidcAuthenticationMechanism implements HttpAuthenticationMechanism {

protected static final String BEARER = "Bearer";

protected volatile OAuth2Auth auth;
protected OidcConfig config;

public AbstractOidcAuthenticationMechanism setAuth(OAuth2Auth auth, OidcConfig config) {
this.auth = auth;
this.config = config;
return this;
}
@Inject
DefaultTenantConfigResolver tenantConfigResolver;

protected CompletionStage<SecurityIdentity> authenticate(IdentityProviderManager identityProviderManager,
TokenCredential token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
@ApplicationScoped
public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {

@Override
public CompletionStage<SecurityIdentity> authenticate(RoutingContext context,
IdentityProviderManager identityProviderManager) {
String token = extractBearerToken(context);

// if a bearer token is provided try to authenticate
if (token != null) {
return authenticate(identityProviderManager, new AccessTokenCredential(token));
return authenticate(identityProviderManager, new AccessTokenCredential(token, context));
}

return CompletableFuture.completedFuture(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha

private static QuarkusSecurityIdentity augmentIdentity(SecurityIdentity securityIdentity,
String accessToken,
String refreshToken) {
String refreshToken,
RoutingContext context) {
final RefreshToken refreshTokenCredential = new RefreshToken(refreshToken);
return QuarkusSecurityIdentity.builder()
.setPrincipal(securityIdentity.getPrincipal())
.addCredentials(securityIdentity.getCredentials())
.addCredential(new AccessTokenCredential(accessToken, refreshTokenCredential))
.addCredential(new AccessTokenCredential(accessToken, refreshTokenCredential, context))
.addCredential(refreshTokenCredential)
.addRoles(securityIdentity.getRoles())
.addAttributes(securityIdentity.getAttributes())
Expand All @@ -68,11 +69,12 @@ public CompletionStage<SecurityIdentity> authenticate(RoutingContext context,
// if session already established, try to re-authenticate
if (sessionCookie != null) {
String[] tokens = sessionCookie.getValue().split(COOKIE_DELIM);
return authenticate(identityProviderManager, new IdTokenCredential(tokens[0]))
return authenticate(identityProviderManager, new IdTokenCredential(tokens[0], context))
.thenCompose(new Function<SecurityIdentity, CompletionStage<SecurityIdentity>>() {
@Override
public CompletionStage<SecurityIdentity> apply(SecurityIdentity securityIdentity) {
return CompletableFuture.completedFuture(augmentIdentity(securityIdentity, tokens[1], tokens[2]));
return CompletableFuture
.completedFuture(augmentIdentity(securityIdentity, tokens[1], tokens[2], context));
}
});
}
Expand All @@ -90,7 +92,7 @@ public CompletionStage<ChallengeData> getChallenge(RoutingContext context) {
List<Object> scopes = new ArrayList<>();

scopes.add("openid");
config.authentication.scopes.ifPresent(scopes::addAll);
tenantConfigResolver.resolve(context).oidcConfig.getAuthentication().scopes.ifPresent(scopes::addAll);

params.put("scopes", new JsonArray(scopes));

Expand All @@ -100,7 +102,8 @@ public CompletionStage<ChallengeData> getChallenge(RoutingContext context) {

params.put("state", generateState(context, dynamicPath));

challenge = new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION, auth.authorizeURL(params));
challenge = new ChallengeData(HttpResponseStatus.FOUND.code(), HttpHeaders.LOCATION,
tenantConfigResolver.resolve(context).auth.authorizeURL(params));

return CompletableFuture.completedFuture(challenge);
}
Expand Down Expand Up @@ -161,13 +164,13 @@ private CompletionStage<SecurityIdentity> performCodeFlow(IdentityProviderManage
params.put("code", code);
params.put("redirect_uri", buildCodeRedirectUri(context, absoluteUri, getDynamicPath(context, absoluteUri)));

auth.authenticate(params, userAsyncResult -> {
tenantConfigResolver.resolve(context).auth.authenticate(params, userAsyncResult -> {
if (userAsyncResult.failed()) {
cf.completeExceptionally(new AuthenticationFailedException());
} else {
AccessToken result = AccessToken.class.cast(userAsyncResult.result());

authenticate(identityProviderManager, new IdTokenCredential(result.opaqueIdToken()))
authenticate(identityProviderManager, new IdTokenCredential(result.opaqueIdToken(), context))
.whenCompleteAsync((securityIdentity, throwable) -> {
if (throwable != null) {
cf.completeExceptionally(throwable);
Expand Down Expand Up @@ -197,12 +200,13 @@ private void processSuccessfulAuthentication(RoutingContext context,
context.response().addCookie(cookie);

cf.complete(augmentIdentity(securityIdentity, result.opaqueAccessToken(),
result.opaqueRefreshToken()));
result.opaqueRefreshToken(), context));
}

private String getDynamicPath(RoutingContext context, URI absoluteUri) {
if (config.authentication.redirectPath.isPresent()) {
String redirectPath = config.authentication.redirectPath.get();
OidcTenantConfig config = tenantConfigResolver.resolve(context).oidcConfig;
if (config.getAuthentication().redirectPath.isPresent()) {
String redirectPath = config.getAuthentication().redirectPath.get();
String requestPath = absoluteUri.getRawPath();
if (requestPath.startsWith(redirectPath) && requestPath.length() > redirectPath.length()) {
return requestPath.substring(redirectPath.length());
Expand Down Expand Up @@ -234,7 +238,7 @@ private String buildCodeRedirectUri(RoutingContext context, URI absoluteUri, Str
.append(absoluteUri.getAuthority());

String path = dynamicPath != null
? config.authentication.redirectPath.get()
? tenantConfigResolver.resolve(context).oidcConfig.getAuthentication().redirectPath.get()
: absoluteUri.getRawPath();

return builder.append(path).toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.oidc.runtime;

import io.quarkus.security.credential.TokenCredential;
import io.vertx.ext.web.RoutingContext;

public class ContextAwareTokenCredential extends TokenCredential {

private RoutingContext context;

protected ContextAwareTokenCredential(String token, String type, RoutingContext context) {
super(token, type);
this.context = context;
}

RoutingContext getContext() {
return context;
}
}
Loading

0 comments on commit 92ab536

Please sign in to comment.