Skip to content

Commit

Permalink
Support for two or more authentications for a single request
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik authored and danielsoro committed Sep 20, 2024
1 parent 57c5ce2 commit d3e2e0f
Show file tree
Hide file tree
Showing 21 changed files with 718 additions and 12 deletions.
64 changes: 64 additions & 0 deletions docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,70 @@ ifndef::no-quarkus-elytron-security-oauth2[ ^|Yes]
If different sources provide the user credentials, you can combine authentication mechanisms.
For example, you can combine the built-in Basic and the Quarkus `quarkus-oidc` Bearer token authentication mechanisms.

The authentication process completes as soon as the first `SecurityIdentity` is produced by one of the authentication mechanisms.

In some cases it can be required that all registered authentication mechanisms create their `SecurityIdentity`.
It can be required when the credentials such as tokens have to be passed over <<mutual-tls>>,
for example, when users are authenticating via `Virtual Private Network`, or when the current token has to be bound
to the client certificate for the token verification to succeed, guaranteeing that the token was issued exactly
to the same client which is currently passing this token to Quarkus alongside its client certificate.

In Quarkus such authentication is called `inclusive` and you can enable it like this:

[source,properties]
----
quarkus.http.auth.inclusive=true
----

If the authentication is inclusive then `SecurityIdentity` created by the first authentication mechanism can be
injected into the application code.
For example, if both <<mutual-tls>> and basic authentication mechanism authentications are required,
the <<mutual-tls>> authentication mechanism will create `SecurityIdentity` first.

Additional `SecurityIdentity` instances can be accessed as a `quarkus.security.identities` attribute on the first
`SecurityIdentity`, however, accessing these extra identities directly may not be necessary, for example,
when both <<mutual-tls>> and xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer authentication] mechanisms
have been combined and done their authentications, the authenticated bearer token can be injected as a token
credential alongside `SecurityIdentity` created by <<mutual-tls>>. This is exemplified below:

[source,java]
----
package org.acme.security;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class InclusiveAuthExampleBean {
@Inject
SecurityIdentity mtlsIdentity; <1>
@Inject
AccessTokenCredential accessTokenCredential;
private AccessTokenCredential getAccessTokenCredential() {
if (doItHardWay()) {
var securityIdentities = HttpSecurityUtils.getSecurityIdentities(mtlsIdentity); <2>
if (securityIdentities != null) {
SecurityIdentity bearerIdentity = securityIdentities.get(OidcConstants.BEARER_SCHEME);
if (bearerIdentity != null) {
return bearerIdentity.getCredential(AccessTokenCredential.class);
}
}
}
return accessTokenCredential;
}
}
----
<1> This is the `SecurityIdentity` created by applicable authentication mechanism with the highest priority.
<2> Other applicable authentication mechanisms performed authentication and are available on the `SecurityIdentity`.

[IMPORTANT]
====
You cannot combine the Quarkus `quarkus-oidc` Bearer token and `smallrye-jwt` authentication mechanisms because both mechanisms attempt to verify the token extracted from the HTTP Bearer token authentication scheme.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ public class OidcConfigurationMetadataProducer {
@Produces
@RequestScoped
OidcConfigurationMetadata produce() {
OidcConfigurationMetadata configMetadata = null;

configMetadata = (OidcConfigurationMetadata) identity.getAttribute(OidcUtils.CONFIG_METADATA_ATTRIBUTE);
OidcConfigurationMetadata configMetadata = OidcUtils.getAttribute(identity, OidcUtils.CONFIG_METADATA_ATTRIBUTE);

if (configMetadata == null && tenantConfig.getDefaultTenant().oidcConfig.tenantEnabled) {
configMetadata = tenantConfig.getDefaultTenant().provider.getMetadata();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private JsonWebToken getTokenCredential(Class<? extends TokenCredential> type) {
&& ((OidcJwtCallerPrincipal) identity.getPrincipal()).getCredential().getClass() == type) {
return (JsonWebToken) identity.getPrincipal();
}
TokenCredential credential = identity.getCredential(type);
TokenCredential credential = OidcUtils.getTokenCredential(identity, type);
if (credential != null && credential.getToken() != null) {
if (credential instanceof AccessTokenCredential && ((AccessTokenCredential) credential).isOpaque()) {
throw new OIDCException("Opaque access token can not be converted to JsonWebToken");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class OidcTokenCredentialProducer {
@Produces
@RequestScoped
IdTokenCredential currentIdToken() {
IdTokenCredential cred = identity.getCredential(IdTokenCredential.class);
IdTokenCredential cred = OidcUtils.getTokenCredential(identity, IdTokenCredential.class);
if (cred == null || cred.getToken() == null) {
LOG.trace("IdToken is null");
cred = new IdTokenCredential();
Expand All @@ -43,7 +43,7 @@ IdTokenCredential currentIdToken() {
@Alternative
@Priority(1)
AccessTokenCredential currentAccessToken() {
AccessTokenCredential cred = identity.getCredential(AccessTokenCredential.class);
AccessTokenCredential cred = OidcUtils.getTokenCredential(identity, AccessTokenCredential.class);
if (cred == null || cred.getToken() == null) {
LOG.trace("AccessToken is null");
cred = new AccessTokenCredential();
Expand All @@ -54,7 +54,7 @@ AccessTokenCredential currentAccessToken() {
@Produces
@RequestScoped
RefreshToken currentRefreshToken() {
RefreshToken cred = identity.getCredential(RefreshToken.class);
RefreshToken cred = OidcUtils.getTokenCredential(identity, RefreshToken.class);
if (cred == null) {
LOG.trace("RefreshToken is null");
cred = new RefreshToken();
Expand All @@ -70,7 +70,7 @@ RefreshToken currentRefreshToken() {
@Produces
@RequestScoped
UserInfo currentUserInfo() {
UserInfo userInfo = (UserInfo) identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE);
UserInfo userInfo = OidcUtils.getAttribute(identity, OidcUtils.USER_INFO_ATTRIBUTE);
if (userInfo == null) {
LOG.trace("UserInfo is null");
userInfo = new UserInfo();
Expand Down Expand Up @@ -106,8 +106,8 @@ TokenIntrospection idTokenIntrospection() {
@Produces
@RequestScoped
TokenIntrospection tokenIntrospection() {
TokenVerificationResult codeFlowAccessTokenResult = (TokenVerificationResult) identity
.getAttribute(OidcUtils.CODE_ACCESS_TOKEN_RESULT);
TokenVerificationResult codeFlowAccessTokenResult = OidcUtils.getAttribute(identity,
OidcUtils.CODE_ACCESS_TOKEN_RESULT);
if (codeFlowAccessTokenResult == null) {
return tokenIntrospectionFromIdentityAttribute();
} else {
Expand All @@ -116,7 +116,7 @@ TokenIntrospection tokenIntrospection() {
}

TokenIntrospection tokenIntrospectionFromIdentityAttribute() {
TokenIntrospection introspection = (TokenIntrospection) identity.getAttribute(OidcUtils.INTROSPECTION_ATTRIBUTE);
TokenIntrospection introspection = OidcUtils.getAttribute(identity, OidcUtils.INTROSPECTION_ATTRIBUTE);
if (introspection == null) {
LOG.trace("TokenIntrospection is null");
introspection = new TokenIntrospection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@
import io.quarkus.security.StringPermission;
import io.quarkus.security.credential.TokenCredential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity.Builder;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.jwt.algorithm.ContentEncryptionAlgorithm;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.mutiny.Uni;
Expand Down Expand Up @@ -814,4 +816,34 @@ public static SecretKey createSecretKeyFromDigest(byte[] secretBytes) {
throw new OIDCException(ex);
}
}

public static <T extends TokenCredential> T getTokenCredential(SecurityIdentity identity, Class<T> type) {
T credential = identity.getCredential(type);
if (credential == null) {
Map<String, SecurityIdentity> identities = HttpSecurityUtils.getSecurityIdentities(identity);
if (identities != null) {
for (String scheme : identities.keySet()) {
if (scheme.equalsIgnoreCase(OidcConstants.BEARER_SCHEME)) {
return identities.get(scheme).getCredential(type);
}
}
}
}
return credential;
}

public static <T> T getAttribute(SecurityIdentity identity, String name) {
T attribute = identity.getAttribute(name);
if (attribute == null) {
Map<String, SecurityIdentity> identities = HttpSecurityUtils.getSecurityIdentities(identity);
if (identities != null) {
for (String scheme : identities.keySet()) {
if (scheme.equalsIgnoreCase(OidcConstants.BEARER_SCHEME)) {
return identities.get(scheme).getAttribute(name);
}
}
}
}
return attribute;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.quarkus.vertx.http.security;

import java.io.File;
import java.util.Set;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.runtime.StartupEvent;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticator;
import io.quarkus.vertx.http.runtime.security.MtlsAuthenticationMechanism;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-test", password = "secret", formats = {
Format.JKS }, client = true))
public class InclusiveAuthValidationTest {

private static final String configuration = """
quarkus.http.auth.inclusive=true
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=secret
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=secret
quarkus.http.ssl.client-auth=REQUEST
quarkus.http.auth.basic=true
quarkus.http.auth.proactive=true
""";

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(TopPriorityAuthMechanism.class, StartupObserver.class)
.addClasses(TestIdentityProvider.class, TestTrustedIdentityProvider.class, TestIdentityController.class)
.addAsResource(new StringAsset(configuration), "application.properties")
.addAsResource(new File("target/certs/mtls-test-keystore.jks"), "server-keystore.jks")
.addAsResource(new File("target/certs/mtls-test-server-truststore.jks"), "server-truststore.jks"))
.assertException(throwable -> {
var errMsg = throwable.getMessage();
Assertions.assertTrue(errMsg.contains("Inclusive authentication is enabled"));
Assertions.assertTrue(errMsg.contains("TopPriorityAuthMechanism"));
});

@Test
public void test() {
Assertions.fail();
}

public static class StartupObserver {
void observe(@Observes StartupEvent startupEvent, HttpAuthenticator authenticator) {
// authenticator is only initialized when required
}
}

@ApplicationScoped
public static class TopPriorityAuthMechanism implements HttpAuthenticationMechanism {

@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return Uni.createFrom().nullItem();
}

@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return Uni.createFrom().nullItem();
}

@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return Set.of(UsernamePasswordAuthenticationRequest.class);
}

@Override
public int getPriority() {
return MtlsAuthenticationMechanism.PRIORITY + 1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,27 @@ public class AuthConfig {
*/
@ConfigItem(defaultValue = "true")
public boolean proactive;

/**
* Require that all registered HTTP authentication mechanisms must complete the authentication.
* <p>
* Typically, this property has to be true when the credentials are carried over mTLS, when both mTLS and another
* authentication, for example, OIDC bearer token authentication, must succeed.
* In such cases, `SecurityIdentity` created by the first mechanism, mTLS, can be injected, identities created
* by other mechanisms will be available on `SecurityIdentity`.
* The identities can be retrieved using utility method as in the example below:
*
* <pre>
* {@code
* io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getSecurityIdentities(securityIdentity)
* }
* </pre>
* <p>
* This property is false by default which means that the authentication process is complete as soon as the first
* `SecurityIdentity` is created.
* <p>
* This property will be ignored if the path specific authentication is enabled.
*/
@ConfigItem(defaultValue = "false")
public boolean inclusive;
}
Loading

0 comments on commit d3e2e0f

Please sign in to comment.