mirror of
https://github.com/jlengrand/quarkus.git
synced 2026-03-10 08:41:22 +00:00
Merge pull request #7086 from sberyozkin/oidc_auth_mechanism
Add a composite OidcAuthenticationMechanism
This commit is contained in:
@@ -173,6 +173,7 @@
|
||||
<spring-security.version>5.2.0.RELEASE</spring-security.version>
|
||||
<spring-boot.version>2.1.10.RELEASE</spring-boot.version>
|
||||
<mvel2.version>2.4.4.Final</mvel2.version>
|
||||
<htmlunit.version>2.36.0</htmlunit.version>
|
||||
<mockito.version>3.2.4</mockito.version>
|
||||
<jna.version>5.3.1</jna.version>
|
||||
<antlr.version>4.7.2</antlr.version>
|
||||
@@ -2111,6 +2112,21 @@
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>${mockito.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>${htmlunit.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.smallrye.reactive</groupId>
|
||||
<artifactId>smallrye-axle-generator</artifactId>
|
||||
|
||||
@@ -12,7 +12,6 @@ import io.quarkus.deployment.builditem.FeatureBuildItem;
|
||||
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerAuthorizer;
|
||||
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerConfig;
|
||||
import io.quarkus.keycloak.pep.runtime.KeycloakPolicyEnforcerRecorder;
|
||||
import io.quarkus.oidc.OIDCException;
|
||||
import io.quarkus.oidc.runtime.OidcBuildTimeConfig;
|
||||
import io.quarkus.oidc.runtime.OidcConfig;
|
||||
import io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem;
|
||||
@@ -70,13 +69,10 @@ public class KeycloakPolicyEnforcerBuildStep {
|
||||
|
||||
@Record(ExecutionTime.RUNTIME_INIT)
|
||||
@BuildStep
|
||||
public void setup(OidcBuildTimeConfig buildTimeConfig, KeycloakPolicyEnforcerConfig keycloakConfig,
|
||||
OidcConfig runTimeConfig, KeycloakPolicyEnforcerRecorder recorder, BeanContainerBuildItem bc) {
|
||||
if (!buildTimeConfig.applicationType.equals(OidcBuildTimeConfig.ApplicationType.SERVICE)) {
|
||||
throw new OIDCException("Application type [" + buildTimeConfig.applicationType + "] not supported");
|
||||
}
|
||||
if (keycloakConfig.policyEnforcer.enable) {
|
||||
recorder.setup(runTimeConfig, keycloakConfig, bc.getValue());
|
||||
public void setup(OidcBuildTimeConfig oidcBuildTimeConfig, OidcConfig oidcRunTimeConfig,
|
||||
KeycloakPolicyEnforcerConfig keycloakConfig, KeycloakPolicyEnforcerRecorder recorder, BeanContainerBuildItem bc) {
|
||||
if (oidcBuildTimeConfig.enabled && keycloakConfig.policyEnforcer.enable) {
|
||||
recorder.setup(oidcRunTimeConfig, keycloakConfig, bc.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package io.quarkus.keycloak.pep.runtime;
|
||||
|
||||
import io.quarkus.arc.runtime.BeanContainer;
|
||||
import io.quarkus.oidc.OIDCException;
|
||||
import io.quarkus.oidc.runtime.OidcConfig;
|
||||
import io.quarkus.oidc.runtime.OidcTenantConfig;
|
||||
import io.quarkus.runtime.annotations.Recorder;
|
||||
|
||||
@Recorder
|
||||
public class KeycloakPolicyEnforcerRecorder {
|
||||
|
||||
public void setup(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config, BeanContainer beanContainer) {
|
||||
if (oidcConfig.defaultTenant.applicationType == OidcTenantConfig.ApplicationType.WEB_APP) {
|
||||
throw new OIDCException("Application type [" + oidcConfig.defaultTenant.applicationType + "] is not supported");
|
||||
}
|
||||
beanContainer.instance(KeycloakPolicyEnforcerAuthorizer.class).init(oidcConfig, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ import io.quarkus.deployment.annotations.ExecutionTime;
|
||||
import io.quarkus.deployment.annotations.Record;
|
||||
import io.quarkus.deployment.builditem.EnableAllSecurityServicesBuildItem;
|
||||
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.OidcAuthenticationMechanism;
|
||||
import io.quarkus.oidc.runtime.OidcBuildTimeConfig;
|
||||
import io.quarkus.oidc.runtime.OidcConfig;
|
||||
import io.quarkus.oidc.runtime.OidcIdentityProvider;
|
||||
@@ -53,12 +52,8 @@ public class OidcBuildStep {
|
||||
public AdditionalBeanBuildItem beans() {
|
||||
AdditionalBeanBuildItem.Builder beans = AdditionalBeanBuildItem.builder().setUnremovable();
|
||||
|
||||
if (OidcBuildTimeConfig.ApplicationType.SERVICE.equals(buildTimeConfig.applicationType)) {
|
||||
beans.addBeanClass(BearerAuthenticationMechanism.class);
|
||||
} else if (OidcBuildTimeConfig.ApplicationType.WEB_APP.equals(buildTimeConfig.applicationType)) {
|
||||
beans.addBeanClass(CodeAuthenticationMechanism.class);
|
||||
}
|
||||
return beans.addBeanClass(OidcJsonWebTokenProducer.class)
|
||||
return beans.addBeanClass(OidcAuthenticationMechanism.class)
|
||||
.addBeanClass(OidcJsonWebTokenProducer.class)
|
||||
.addBeanClass(OidcTokenCredentialProducer.class)
|
||||
.addBeanClass(OidcIdentityProvider.class)
|
||||
.addBeanClass(DefaultTenantConfigResolver.class).build();
|
||||
|
||||
@@ -2,21 +2,12 @@ package io.quarkus.oidc.runtime;
|
||||
|
||||
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;
|
||||
|
||||
abstract class AbstractOidcAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||
|
||||
protected static final String BEARER = "Bearer";
|
||||
|
||||
@Inject
|
||||
DefaultTenantConfigResolver tenantConfigResolver;
|
||||
|
||||
abstract class AbstractOidcAuthenticationMechanism {
|
||||
protected CompletionStage<SecurityIdentity> authenticate(IdentityProviderManager identityProviderManager,
|
||||
TokenCredential token) {
|
||||
return identityProviderManager.authenticate(new TokenAuthenticationRequest(token));
|
||||
|
||||
@@ -3,8 +3,6 @@ package io.quarkus.oidc.runtime;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.quarkus.oidc.AccessTokenCredential;
|
||||
import io.quarkus.security.identity.IdentityProviderManager;
|
||||
@@ -14,12 +12,13 @@ import io.vertx.core.http.HttpHeaders;
|
||||
import io.vertx.core.http.HttpServerRequest;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
@ApplicationScoped
|
||||
public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {
|
||||
|
||||
@Override
|
||||
private static final String BEARER = "Bearer";
|
||||
|
||||
public CompletionStage<SecurityIdentity> authenticate(RoutingContext context,
|
||||
IdentityProviderManager identityProviderManager) {
|
||||
IdentityProviderManager identityProviderManager,
|
||||
DefaultTenantConfigResolver resolver) {
|
||||
String token = extractBearerToken(context);
|
||||
|
||||
// if a bearer token is provided try to authenticate
|
||||
@@ -30,8 +29,7 @@ public class BearerAuthenticationMechanism extends AbstractOidcAuthenticationMec
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<ChallengeData> getChallenge(RoutingContext context) {
|
||||
public CompletionStage<ChallengeData> getChallenge(RoutingContext context, DefaultTenantConfigResolver resolver) {
|
||||
String bearerToken = extractBearerToken(context);
|
||||
|
||||
if (bearerToken == null) {
|
||||
|
||||
@@ -10,8 +10,6 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
@@ -33,7 +31,6 @@ import io.vertx.ext.auth.oauth2.AccessToken;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
import io.vertx.ext.web.impl.CookieImpl;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMechanism {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class);
|
||||
@@ -63,9 +60,9 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<SecurityIdentity> authenticate(RoutingContext context,
|
||||
IdentityProviderManager identityProviderManager) {
|
||||
IdentityProviderManager identityProviderManager,
|
||||
DefaultTenantConfigResolver resolver) {
|
||||
Cookie sessionCookie = context.request().getCookie(SESSION_COOKIE_NAME);
|
||||
|
||||
// if session already established, try to re-authenticate
|
||||
@@ -82,18 +79,17 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
|
||||
}
|
||||
|
||||
// start a new session by starting the code flow dance
|
||||
return performCodeFlow(identityProviderManager, context);
|
||||
return performCodeFlow(identityProviderManager, context, resolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<ChallengeData> getChallenge(RoutingContext context) {
|
||||
public CompletionStage<ChallengeData> getChallenge(RoutingContext context, DefaultTenantConfigResolver resolver) {
|
||||
removeCookie(context, SESSION_COOKIE_NAME);
|
||||
|
||||
TenantConfigContext configContext = resolver.resolve(context, false);
|
||||
|
||||
ChallengeData challenge;
|
||||
JsonObject params = new JsonObject();
|
||||
|
||||
TenantConfigContext configContext = tenantConfigResolver.resolve(context);
|
||||
|
||||
// scope
|
||||
List<Object> scopes = new ArrayList<>();
|
||||
scopes.add("openid");
|
||||
@@ -102,13 +98,13 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
|
||||
|
||||
// redirect_uri
|
||||
URI absoluteUri = URI.create(context.request().absoluteURI());
|
||||
String redirectPath = getRedirectPath(context, absoluteUri);
|
||||
String redirectPath = getRedirectPath(configContext, absoluteUri);
|
||||
String redirectUriParam = buildRedirectUri(context, absoluteUri, redirectPath);
|
||||
LOG.debugf("Authentication request redirect_uri parameter: %s", redirectUriParam);
|
||||
params.put("redirect_uri", redirectUriParam);
|
||||
|
||||
// state
|
||||
params.put("state", generateState(context, absoluteUri, redirectPath));
|
||||
params.put("state", generateState(context, configContext, absoluteUri, redirectPath));
|
||||
|
||||
// extra redirect parameters, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequests
|
||||
if (configContext.oidcConfig.authentication.getExtraParams() != null) {
|
||||
@@ -124,7 +120,9 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
|
||||
}
|
||||
|
||||
private CompletionStage<SecurityIdentity> performCodeFlow(IdentityProviderManager identityProviderManager,
|
||||
RoutingContext context) {
|
||||
RoutingContext context, DefaultTenantConfigResolver resolver) {
|
||||
TenantConfigContext configContext = resolver.resolve(context, true);
|
||||
|
||||
JsonObject params = new JsonObject();
|
||||
|
||||
String code = context.request().getParam("code");
|
||||
@@ -177,12 +175,12 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
|
||||
}
|
||||
|
||||
params.put("code", code);
|
||||
String redirectPath = getRedirectPath(context, absoluteUri);
|
||||
String redirectPath = getRedirectPath(configContext, absoluteUri);
|
||||
String redirectUriParam = buildRedirectUri(context, absoluteUri, redirectPath);
|
||||
LOG.debugf("Token request redirect_uri parameter: %s", redirectUriParam);
|
||||
params.put("redirect_uri", redirectUriParam);
|
||||
|
||||
tenantConfigResolver.resolve(context).auth.authenticate(params, userAsyncResult -> {
|
||||
configContext.auth.authenticate(params, userAsyncResult -> {
|
||||
if (userAsyncResult.failed()) {
|
||||
cf.completeExceptionally(new AuthenticationFailedException());
|
||||
} else {
|
||||
@@ -221,16 +219,17 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
|
||||
result.opaqueRefreshToken(), context));
|
||||
}
|
||||
|
||||
private String getRedirectPath(RoutingContext context, URI absoluteUri) {
|
||||
Authentication auth = tenantConfigResolver.resolve(context).oidcConfig.getAuthentication();
|
||||
private String getRedirectPath(TenantConfigContext configContext, URI absoluteUri) {
|
||||
Authentication auth = configContext.oidcConfig.getAuthentication();
|
||||
return auth.getRedirectPath().isPresent() ? auth.getRedirectPath().get() : absoluteUri.getRawPath();
|
||||
}
|
||||
|
||||
private String generateState(RoutingContext context, URI absoluteUri, String redirectPath) {
|
||||
private String generateState(RoutingContext context, TenantConfigContext configContext, URI absoluteUri,
|
||||
String redirectPath) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
String cookieValue = uuid;
|
||||
|
||||
Authentication auth = tenantConfigResolver.resolve(context).oidcConfig.getAuthentication();
|
||||
Authentication auth = configContext.oidcConfig.getAuthentication();
|
||||
if (auth.isRestorePathAfterRedirect() && !redirectPath.equals(absoluteUri.getRawPath())) {
|
||||
cookieValue += (COOKIE_DELIM + absoluteUri.getRawPath());
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.quarkus.oidc.runtime;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.enterprise.inject.Instance;
|
||||
import javax.inject.Inject;
|
||||
@@ -26,28 +27,33 @@ public class DefaultTenantConfigResolver {
|
||||
private volatile TenantConfigContext defaultTenant;
|
||||
private volatile Function<OidcTenantConfig, TenantConfigContext> tenantConfigContextFactory;
|
||||
|
||||
TenantConfigContext resolve(RoutingContext context) {
|
||||
@PostConstruct
|
||||
public void verifyResolvers() {
|
||||
if (tenantConfigResolver.isAmbiguous()) {
|
||||
throw new IllegalStateException("Multiple " + TenantConfigResolver.class + " beans registered");
|
||||
}
|
||||
|
||||
TenantConfigContext config = getTenantConfigFromConfigResolver(context, true);
|
||||
|
||||
if (config != null) {
|
||||
return config;
|
||||
}
|
||||
|
||||
String tenant = null;
|
||||
|
||||
if (tenantResolver.isAmbiguous()) {
|
||||
throw new IllegalStateException("Multiple " + TenantResolver.class + " beans registered");
|
||||
}
|
||||
}
|
||||
|
||||
if (tenantResolver.isResolvable()) {
|
||||
tenant = tenantResolver.get().resolve(context);
|
||||
/**
|
||||
* Resolve {@linkplain TenantConfigContext} which contains the tenant configuration and
|
||||
* the active OIDC connection instance which may be null.
|
||||
*
|
||||
* @param context the current request context
|
||||
* @param create if true then the OIDC connection must be available or established
|
||||
* for the resolution to be successful
|
||||
* @return
|
||||
*/
|
||||
TenantConfigContext resolve(RoutingContext context, boolean create) {
|
||||
TenantConfigContext config = getTenantConfigFromConfigResolver(context, create);
|
||||
|
||||
if (config == null) {
|
||||
config = getTenantConfigFromTenantResolver(context);
|
||||
}
|
||||
|
||||
return tenantsConfig.getOrDefault(tenant, defaultTenant);
|
||||
return config;
|
||||
}
|
||||
|
||||
void setTenantsConfig(Map<String, TenantConfigContext> tenantsConfig) {
|
||||
@@ -62,6 +68,16 @@ public class DefaultTenantConfigResolver {
|
||||
this.tenantConfigContextFactory = tenantConfigContextFactory;
|
||||
}
|
||||
|
||||
private TenantConfigContext getTenantConfigFromTenantResolver(RoutingContext context) {
|
||||
String tenant = null;
|
||||
|
||||
if (tenantResolver.isResolvable()) {
|
||||
tenant = tenantResolver.get().resolve(context);
|
||||
}
|
||||
|
||||
return tenantsConfig.getOrDefault(tenant, defaultTenant);
|
||||
}
|
||||
|
||||
boolean isBlocking(RoutingContext context) {
|
||||
return getTenantConfigFromConfigResolver(context, false) == null;
|
||||
}
|
||||
@@ -78,8 +94,8 @@ public class DefaultTenantConfigResolver {
|
||||
}
|
||||
|
||||
if (tenantConfig != null) {
|
||||
String tenantId = tenantConfig.getClientId()
|
||||
.orElseThrow(() -> new IllegalStateException("You must provide a client_id"));
|
||||
String tenantId = tenantConfig.getTenantId()
|
||||
.orElseThrow(() -> new IllegalStateException("You must provide a tenant id"));
|
||||
TenantConfigContext tenantContext = tenantsConfig.get(tenantId);
|
||||
|
||||
if (tenantContext == null && create) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.quarkus.oidc.runtime;
|
||||
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
import javax.enterprise.context.ApplicationScoped;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.quarkus.security.identity.IdentityProviderManager;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import io.quarkus.vertx.http.runtime.security.ChallengeData;
|
||||
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
|
||||
import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
@ApplicationScoped
|
||||
public class OidcAuthenticationMechanism implements HttpAuthenticationMechanism {
|
||||
|
||||
@Inject
|
||||
DefaultTenantConfigResolver resolver;
|
||||
private BearerAuthenticationMechanism bearerAuth = new BearerAuthenticationMechanism();
|
||||
private CodeAuthenticationMechanism codeAuth = new CodeAuthenticationMechanism();
|
||||
|
||||
@Override
|
||||
public CompletionStage<SecurityIdentity> authenticate(RoutingContext context,
|
||||
IdentityProviderManager identityProviderManager) {
|
||||
return isWebApp(context) ? codeAuth.authenticate(context, identityProviderManager, resolver)
|
||||
: bearerAuth.authenticate(context, identityProviderManager, resolver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<ChallengeData> getChallenge(RoutingContext context) {
|
||||
return isWebApp(context) ? codeAuth.getChallenge(context, resolver)
|
||||
: bearerAuth.getChallenge(context, resolver);
|
||||
}
|
||||
|
||||
private boolean isWebApp(RoutingContext context) {
|
||||
return OidcTenantConfig.ApplicationType.WEB_APP == resolver.resolve(context, false).oidcConfig.applicationType;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,26 +13,4 @@ public class OidcBuildTimeConfig {
|
||||
*/
|
||||
@ConfigItem(defaultValue = "true")
|
||||
public boolean enabled;
|
||||
|
||||
/**
|
||||
* The application type, which can be one of the following values from enum {@link ApplicationType}.
|
||||
*/
|
||||
@ConfigItem(defaultValue = "service")
|
||||
public ApplicationType applicationType;
|
||||
|
||||
public enum ApplicationType {
|
||||
/**
|
||||
* A {@code WEB_APP} is a client that server pages, usually a frontend application. For this type of client the
|
||||
* Authorization Code Flow is
|
||||
* defined as the preferred method for authenticating users.
|
||||
*/
|
||||
WEB_APP,
|
||||
|
||||
/**
|
||||
* A {@code SERVICE} is a client that has a set of protected HTTP resources, usually a backend application following the
|
||||
* RESTful Architectural Design. For this type of client, the Bearer Authorization method is defined as the preferred
|
||||
* method for authenticating and authorizing users.
|
||||
*/
|
||||
SERVICE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import io.vertx.ext.web.RoutingContext;
|
||||
|
||||
@ApplicationScoped
|
||||
public class OidcIdentityProvider implements IdentityProvider<TokenAuthenticationRequest> {
|
||||
|
||||
@Inject
|
||||
DefaultTenantConfigResolver tenantResolver;
|
||||
|
||||
@@ -36,7 +35,6 @@ public class OidcIdentityProvider implements IdentityProvider<TokenAuthenticatio
|
||||
return TokenAuthenticationRequest.class;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public CompletionStage<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
|
||||
AuthenticationRequestContext context) {
|
||||
@@ -55,10 +53,11 @@ public class OidcIdentityProvider implements IdentityProvider<TokenAuthenticatio
|
||||
return authenticate(request, vertxContext);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private CompletableFuture<SecurityIdentity> authenticate(TokenAuthenticationRequest request,
|
||||
RoutingContext vertxContext) {
|
||||
CompletableFuture<SecurityIdentity> result = new CompletableFuture<>();
|
||||
TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext);
|
||||
TenantConfigContext resolvedContext = tenantResolver.resolve(vertxContext, true);
|
||||
OidcTenantConfig config = resolvedContext.oidcConfig;
|
||||
|
||||
resolvedContext.auth.decodeToken(request.getToken().getToken(),
|
||||
@@ -66,7 +65,7 @@ public class OidcIdentityProvider implements IdentityProvider<TokenAuthenticatio
|
||||
@Override
|
||||
public void handle(AsyncResult<AccessToken> event) {
|
||||
if (event.failed()) {
|
||||
result.completeExceptionally(new AuthenticationFailedException());
|
||||
result.completeExceptionally(new AuthenticationFailedException(event.cause()));
|
||||
return;
|
||||
}
|
||||
AccessToken token = event.result();
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import io.quarkus.oidc.AccessTokenCredential;
|
||||
import io.quarkus.oidc.IdToken;
|
||||
import io.quarkus.oidc.IdTokenCredential;
|
||||
import io.quarkus.oidc.OIDCException;
|
||||
import io.quarkus.security.credential.TokenCredential;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import io.smallrye.jwt.auth.cdi.NullJsonWebToken;
|
||||
@@ -67,11 +68,12 @@ public class OidcJsonWebTokenProducer {
|
||||
.setSkipAllValidators()
|
||||
.build().processToClaims(credential.getToken());
|
||||
} catch (InvalidJwtException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new OIDCException(e);
|
||||
}
|
||||
jwtClaims.setClaim(Claims.raw_token.name(), credential.getToken());
|
||||
return new OidcJwtCallerPrincipal(jwtClaims, credential);
|
||||
}
|
||||
throw new IllegalStateException("Current identity not associated with an access token");
|
||||
String tokenType = type == AccessTokenCredential.class ? "access" : "ID";
|
||||
throw new OIDCException("Current identity is not associated with an " + tokenType + " token");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ public class OidcRecorder {
|
||||
Map<String, TenantConfigContext> tenantsConfig = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, OidcTenantConfig> tenant : config.namedTenants.entrySet()) {
|
||||
if (config.defaultTenant.getTenantId().isPresent()
|
||||
&& tenant.getKey().equals(config.defaultTenant.getTenantId().get())) {
|
||||
throw new OIDCException("tenant-id '" + tenant.getKey() + "' duplicates the default tenant-id");
|
||||
}
|
||||
if (tenant.getValue().getTenantId().isPresent() && !tenant.getKey().equals(tenant.getValue().getTenantId().get())) {
|
||||
throw new OIDCException("Configuration has 2 different tenant-id values: '"
|
||||
+ tenant.getKey() + "' and '" + tenant.getValue().getTenantId().get() + "'");
|
||||
}
|
||||
tenantsConfig.put(tenant.getKey(), createTenantContext(vertxValue, tenant.getValue()));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,19 @@ import io.quarkus.runtime.annotations.ConfigItem;
|
||||
@ConfigGroup
|
||||
public class OidcTenantConfig {
|
||||
|
||||
/**
|
||||
* A unique tenant identifier. It must be set by {@code TenantConfigResolver} providers which
|
||||
* resolve the tenant configuration dynamically and is optional in all other cases.
|
||||
*/
|
||||
@ConfigItem
|
||||
Optional<String> tenantId = Optional.empty();
|
||||
|
||||
/**
|
||||
* The application type, which can be one of the following values from enum {@link ApplicationType}.
|
||||
*/
|
||||
@ConfigItem(defaultValue = "service")
|
||||
public ApplicationType applicationType;
|
||||
|
||||
/**
|
||||
* The maximum amount of time the adapter will try connecting to the currently unavailable OIDC server for.
|
||||
* For example, setting it to '20S' will let the adapter keep requesting the connection for up to 20 seconds.
|
||||
@@ -147,6 +160,14 @@ public class OidcTenantConfig {
|
||||
this.authentication = authentication;
|
||||
}
|
||||
|
||||
public Optional<String> getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(String tenantId) {
|
||||
this.tenantId = Optional.of(tenantId);
|
||||
}
|
||||
|
||||
@ConfigGroup
|
||||
public static class Credentials {
|
||||
|
||||
@@ -356,4 +377,20 @@ public class OidcTenantConfig {
|
||||
this.principalClaim = Optional.of(principalClaim);
|
||||
}
|
||||
}
|
||||
|
||||
public static enum ApplicationType {
|
||||
/**
|
||||
* A {@code WEB_APP} is a client that server pages, usually a frontend application. For this type of client the
|
||||
* Authorization Code Flow is
|
||||
* defined as the preferred method for authenticating users.
|
||||
*/
|
||||
WEB_APP,
|
||||
|
||||
/**
|
||||
* A {@code SERVICE} is a client that has a set of protected HTTP resources, usually a backend application following the
|
||||
* RESTful Architectural Design. For this type of client, the Bearer Authorization method is defined as the preferred
|
||||
* method for authenticating and authorizing users.
|
||||
*/
|
||||
SERVICE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
<properties>
|
||||
<keycloak.url>http://localhost:8180/auth</keycloak.url>
|
||||
<htmlunit.version>2.36.0</htmlunit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -53,18 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>${htmlunit.version}</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ quarkus.oidc.tenant-1.credentials.secret=secret
|
||||
quarkus.oidc.tenant-1.token.issuer=${keycloak.url}/realms/quarkus
|
||||
quarkus.oidc.tenant-1.authentication.redirect-path=/web-app/callback-after-redirect
|
||||
quarkus.oidc.tenant-1.authentication.restore-path-after-redirect=false
|
||||
quarkus.oidc.tenant-1.application-type=web-app
|
||||
|
||||
quarkus.http.auth.permission.roles1.paths=/index.html
|
||||
quarkus.http.auth.permission.roles1.policy=authenticated
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -12,7 +12,8 @@ public class CustomTenantConfigResolver implements TenantConfigResolver {
|
||||
public OidcTenantConfig resolve(RoutingContext context) {
|
||||
if ("tenant-d".equals(context.request().path().split("/")[2])) {
|
||||
OidcTenantConfig config = new OidcTenantConfig();
|
||||
|
||||
config.setTenantId("tenant-id");
|
||||
;
|
||||
config.setAuthServerUrl(getIssuerUrl() + "/realms/quarkus-d");
|
||||
config.setClientId("quarkus-d");
|
||||
OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials();
|
||||
|
||||
@@ -1,38 +1,67 @@
|
||||
package io.quarkus.it.keycloak;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.security.RolesAllowed;
|
||||
import javax.inject.Inject;
|
||||
import javax.json.JsonString;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
|
||||
import io.quarkus.oidc.IdToken;
|
||||
import io.quarkus.oidc.OIDCException;
|
||||
|
||||
@Path("/tenant/{tenant}/api/user")
|
||||
public class TenantResource {
|
||||
|
||||
@Inject
|
||||
JsonWebToken jwt;
|
||||
JsonWebToken accessToken;
|
||||
|
||||
@Inject
|
||||
@IdToken
|
||||
JsonWebToken idToken;
|
||||
|
||||
@GET
|
||||
@RolesAllowed("user")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Map<String, Object> permissions(@PathParam("tenant") String tenant) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
for (String i : jwt.getClaimNames()) {
|
||||
claims.put(i, fromJsonWebObject(jwt.getClaim(i)));
|
||||
}
|
||||
return claims;
|
||||
public String userName(@PathParam("tenant") String tenant) {
|
||||
return tenant + ":" + ("tenant-web-app".equals(tenant) ? getNameWebAppType() : getNameServiceType());
|
||||
}
|
||||
|
||||
private Object fromJsonWebObject(Object claim) {
|
||||
return claim instanceof JsonString ? ((JsonString) claim).getString() : claim != null ? claim.toString() : null;
|
||||
private String getNameWebAppType() {
|
||||
if (!"ID".equals(idToken.getClaim("typ"))) {
|
||||
throw new OIDCException("Wrong ID token type");
|
||||
}
|
||||
String name = idToken.getName();
|
||||
// The test is set up to use 'upn' for the 'web-app' application type
|
||||
if (!name.equals(idToken.getClaim("upn"))) {
|
||||
throw new OIDCException("upn claim is missing");
|
||||
}
|
||||
// Access token must be available too
|
||||
if (!"Bearer".equals(accessToken.getClaim("typ"))) {
|
||||
throw new OIDCException("Wrong access token type");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private String getNameServiceType() {
|
||||
if (!"Bearer".equals(accessToken.getClaim("typ"))) {
|
||||
throw new OIDCException("Wrong access token type");
|
||||
}
|
||||
String name = null;
|
||||
try {
|
||||
name = idToken.getName();
|
||||
} catch (OIDCException ex) {
|
||||
// expected because an ID token must not be available
|
||||
}
|
||||
if (name != null) {
|
||||
throw new OIDCException("Only the access token can be availabe with the 'service' application type");
|
||||
}
|
||||
name = accessToken.getName();
|
||||
// The test is set up to use 'upn' for the 'web-app' application type
|
||||
if (!name.equals(accessToken.getClaim("preferred_username"))) {
|
||||
throw new OIDCException("preferred_username claim is missing");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,15 +4,24 @@ quarkus.http.cors=true
|
||||
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus-a
|
||||
quarkus.oidc.client-id=quarkus-app-a
|
||||
quarkus.oidc.credentials.secret=secret
|
||||
quarkus.oidc.tenant-web-app.application-type=service
|
||||
|
||||
# Tenant B
|
||||
quarkus.oidc.tenant-b.auth-server-url=${keycloak.url}/realms/quarkus-b
|
||||
quarkus.oidc.tenant-b.client-id=quarkus-app-b
|
||||
quarkus.oidc.tenant-b.credentials.secret=secret
|
||||
quarkus.oidc.tenant-b.token.issuer=${keycloak.url}/realms/quarkus-b
|
||||
quarkus.oidc.tenant-web-app.application-type=service
|
||||
|
||||
# Tenant C
|
||||
quarkus.oidc.tenant-c.auth-server-url=${keycloak.url}/realms/quarkus-c
|
||||
quarkus.oidc.tenant-c.client-id=quarkus-app-c
|
||||
quarkus.oidc.tenant-c.credentials.secret=secret
|
||||
quarkus.oidc.tenant-c.token.audience=${keycloak.url}/realms/quarkus-c
|
||||
quarkus.oidc.tenant-web-app.application-type=service
|
||||
|
||||
# Tenant Web App
|
||||
quarkus.oidc.tenant-web-app.auth-server-url=${keycloak.url}/realms/quarkus-webapp
|
||||
quarkus.oidc.tenant-web-app.client-id=quarkus-app-webapp
|
||||
quarkus.oidc.tenant-web-app.credentials.secret=secret
|
||||
quarkus.oidc.tenant-web-app.application-type=web-app
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
package io.quarkus.it.keycloak;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlForm;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.gargoylesoftware.htmlunit.util.Cookie;
|
||||
|
||||
import io.quarkus.test.common.QuarkusTestResource;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.RestAssured;
|
||||
@@ -19,13 +29,29 @@ public class BearerTokenAuthorizationTest {
|
||||
private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth");
|
||||
private static final String KEYCLOAK_REALM = "quarkus-";
|
||||
|
||||
@Test
|
||||
public void testResolveTenantIdentifierWebApp() throws IOException {
|
||||
try (final WebClient webClient = createWebClient()) {
|
||||
HtmlPage page = webClient.getPage("http://localhost:8081/tenant/tenant-web-app/api/user");
|
||||
// State cookie is available but there must be no saved path parameter
|
||||
// as the tenant-web-app configuration does not set a redirect-path property
|
||||
assertNull(getStateCookieSavedPath(webClient));
|
||||
assertEquals("Log in to quarkus-webapp", page.getTitleText());
|
||||
HtmlForm loginForm = page.getForms().get(0);
|
||||
loginForm.getInputByName("username").setValueAttribute("alice");
|
||||
loginForm.getInputByName("password").setValueAttribute("alice");
|
||||
page = loginForm.getInputByName("login").click();
|
||||
assertEquals("tenant-web-app:alice", page.getBody().asText());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveTenantIdentifier() {
|
||||
RestAssured.given().auth().oauth2(getAccessToken("alice", "b"))
|
||||
.when().get("/tenant/tenant-b/api/user")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("preferred_username", equalTo("alice"));
|
||||
.body(equalTo("tenant-b:alice"));
|
||||
|
||||
// should give a 403 given that access token from issuer b can not access tenant c
|
||||
RestAssured.given().auth().oauth2(getAccessToken("alice", "b"))
|
||||
@@ -40,7 +66,7 @@ public class BearerTokenAuthorizationTest {
|
||||
.when().get("/tenant/tenant-d/api/user")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("preferred_username", equalTo("alice"));
|
||||
.body(equalTo("tenant-d:alice"));
|
||||
|
||||
// should give a 403 given that access token from issuer b can not access tenant c
|
||||
RestAssured.given().auth().oauth2(getAccessToken("alice", "b"))
|
||||
@@ -56,7 +82,7 @@ public class BearerTokenAuthorizationTest {
|
||||
.when().get("/tenant/tenant-any/api/user")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("preferred_username", equalTo("alice"));
|
||||
.body(equalTo("tenant-any:alice"));
|
||||
}
|
||||
|
||||
private String getAccessToken(String userName, String clientId) {
|
||||
@@ -71,4 +97,19 @@ public class BearerTokenAuthorizationTest {
|
||||
.post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + clientId + "/protocol/openid-connect/token")
|
||||
.as(AccessTokenResponse.class).getToken();
|
||||
}
|
||||
|
||||
private WebClient createWebClient() {
|
||||
WebClient webClient = new WebClient();
|
||||
webClient.setCssErrorHandler(new SilentCssErrorHandler());
|
||||
return webClient;
|
||||
}
|
||||
|
||||
private Cookie getStateCookie(WebClient webClient) {
|
||||
return webClient.getCookieManager().getCookie("q_auth");
|
||||
}
|
||||
|
||||
private String getStateCookieSavedPath(WebClient webClient) {
|
||||
String[] parts = getStateCookie(webClient).getValue().split("___");
|
||||
return parts.length == 2 ? parts[1] : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl
|
||||
|
||||
@Override
|
||||
public Map<String, String> start() {
|
||||
for (String realmId : Arrays.asList("a", "b", "c", "d")) {
|
||||
for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp")) {
|
||||
RealmRepresentation realm = createRealm(KEYCLOAK_REALM + realmId);
|
||||
|
||||
realm.getClients().add(createClient("quarkus-app-" + realmId));
|
||||
@@ -92,7 +92,10 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl
|
||||
client.setDirectAccessGrantsEnabled(true);
|
||||
client.setEnabled(true);
|
||||
client.setDefaultRoles(new String[] { "role-" + clientId });
|
||||
|
||||
if ("quarkus-app-webapp".equals(clientId)) {
|
||||
client.setRedirectUris(Arrays.asList("*"));
|
||||
client.setDefaultClientScopes(Arrays.asList("microprofile-jwt"));
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -117,7 +120,7 @@ public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycl
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
for (String realmId : Arrays.asList("a", "b", "c", "d")) {
|
||||
for (String realmId : Arrays.asList("a", "b", "c", "d", "webapp")) {
|
||||
RestAssured
|
||||
.given()
|
||||
.auth().oauth2(getAdminAccessToken())
|
||||
|
||||
Reference in New Issue
Block a user