diff --git a/docs/src/main/asciidoc/security-customization.adoc b/docs/src/main/asciidoc/security-customization.adoc index cc9a55cc7f9eb..cec46ef170277 100644 --- a/docs/src/main/asciidoc/security-customization.adoc +++ b/docs/src/main/asciidoc/security-customization.adoc @@ -56,13 +56,69 @@ public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism } @Override - public HttpCredentialTransport getCredentialTransport() { + public Uni getCredentialTransport() { return delegate.getCredentialTransport(); } } ---- +== Dealing with more than one HttpAuthenticationMechanism + +More than one `HttpAuthenticationMechanism` can be combined, for example, the built-in `Basic` or `JWT` mechanism provided by `quarkus-smallrye-jwt` has to be used to verify the service clients credentials passed as the HTTP `Authorization` `Basic` or `Bearer` scheme values while the `Authorization Code` mechanism provided by `quarkus-oidc` has to be used to authenticate the users with `Keycloak` or other `OpenId Connect` providers. + +In such cases the mechanisms are asked to verify the credentials in turn until a `SecurityIdentity` is created. The mechanisms are sorted in the descending order using their priority. `Basic` authentication mechanism has the highest priority of `2000`, followed by the `Authorization Code` one with the priority of `1001`, with all other mechanisms provided by Quarkus having the priority of `1000`. + +If no credentials are provided then the mechanism specific challenge is created, for example, `401` status is returned by either `Basic` or `JWT` mechanisms, URL redirecting the user to the `OpenId Connect` provider is returned by `quarkus-oidc`, etc. + +So if `Basic` and `Authorization Code` mechanisms are combined then `401` will be returned if no credentials are provided and if `JWT` and `Authorization Code` mechanisms are combined then a redirect URL will be returned. + +In some cases such a default logic of selecting the challenge is exactly what is required by a given application but sometimes it may not meet the requirements. In such cases (or indeed in other similar cases where you'd like to change the order in which the mechanisms are asked to handle the current authentication or challenge request), you can create a custom mechanism and choose which mechanism should create a challenge, for example: + +[source,java] +---- +@ApplicationScoped +public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism { + + private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class); + + @Inject + JWTAuthMechanism jwt; + + @Inject + OidcAuthenticationMechanism oidc; + + @Override + public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { + return selectBetweenJwtAndOidc(context).authenticate(context, identityProviderManager); + } + + @Override + public Uni getChallenge(RoutingContext context) { + return selectBetweenJwtAndOidcChallenge(context).getChallenge(context); + } + + @Override + public Set> getCredentialTypes() { + return selectBetweenJwtAndOidc(context).getCredentialTypes(); + } + + @Override + public HttpCredentialTransport getCredentialTransport(RoutingContext context) { + return selectBetweenJwtAndOidc(context).getCredentialTransport(); + } + + private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) { + .... + } + + private HttpAuthenticationMechanism selectBetweenJwtAndOidcChallenge(RoutingContext context) { + // for example, if no `Authorization` header is available and no `code` parameter is provided - use `jwt` to create a challenge + } + +} +---- + [[security-identity-customization]] == Security Identity Customization diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java index 50452203a916d..11176370f2250 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractOidcAuthenticationMechanism.java @@ -4,20 +4,24 @@ 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.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; abstract class AbstractOidcAuthenticationMechanism { protected DefaultTenantConfigResolver resolver; + private HttpAuthenticationMechanism parent; protected Uni authenticate(IdentityProviderManager identityProviderManager, RoutingContext context, TokenCredential token) { + context.put(HttpAuthenticationMechanism.class.getName(), parent); return identityProviderManager.authenticate(HttpSecurityUtils.setRoutingContextAttribute( new TokenAuthenticationRequest(token), context)); } - void setResolver(DefaultTenantConfigResolver resolver) { + void init(HttpAuthenticationMechanism parent, DefaultTenantConfigResolver resolver) { + this.parent = parent; this.resolver = resolver; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java index 5c0d132e699b3..9b10f8f09d77b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcAuthenticationMechanism.java @@ -22,16 +22,19 @@ @ApplicationScoped public class OidcAuthenticationMechanism implements HttpAuthenticationMechanism { + private static HttpCredentialTransport OIDC_SERVICE_TRANSPORT = new HttpCredentialTransport( + HttpCredentialTransport.Type.AUTHORIZATION, OidcConstants.BEARER_SCHEME); + private static HttpCredentialTransport OIDC_WEB_APP_TRANSPORT = new HttpCredentialTransport( + HttpCredentialTransport.Type.AUTHORIZATION_CODE, OidcConstants.CODE_FLOW_CODE); private final BearerAuthenticationMechanism bearerAuth = new BearerAuthenticationMechanism(); private final CodeAuthenticationMechanism codeAuth = new CodeAuthenticationMechanism(); - private final DefaultTenantConfigResolver resolver; public OidcAuthenticationMechanism(DefaultTenantConfigResolver resolver) { this.resolver = resolver; - this.bearerAuth.setResolver(resolver); - this.codeAuth.setResolver(resolver); + this.bearerAuth.init(this, resolver); + this.codeAuth.init(this, resolver); } @Override @@ -91,10 +94,18 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { - //not 100% correct, but enough for now - //if OIDC is present we don't really want another bearer mechanism - return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, OidcConstants.BEARER_SCHEME); + public Uni getCredentialTransport(RoutingContext context) { + setTenantIdAttribute(context); + return resolve(context).onItem().transform(new Function() { + @Override + public HttpCredentialTransport apply(OidcTenantConfig oidcTenantConfig) { + if (!oidcTenantConfig.tenantEnabled) { + return null; + } + return isWebApp(context, oidcTenantConfig) ? OIDC_WEB_APP_TRANSPORT + : OIDC_SERVICE_TRANSPORT; + } + }); } private static void setTenantIdAttribute(RoutingContext context) { @@ -119,4 +130,9 @@ private static void setTenantIdAttribute(RoutingContext context, String cookiePr context.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId); } } + + @Override + public int getPriority() { + return HttpAuthenticationMechanism.DEFAULT_PRIORITY + 1; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java index 0252ac0d1ba12..216b997ce31aa 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java @@ -58,6 +58,9 @@ public Class getRequestType() { @Override public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { + if (!(request.getToken() instanceof AccessTokenCredential || request.getToken() instanceof IdTokenCredential)) { + return Uni.createFrom().nullItem(); + } RoutingContext vertxContext = HttpSecurityUtils.getRoutingContextAttribute(request); vertxContext.put(AuthenticationRequestContext.class.getName(), context); diff --git a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenRealmUnitTest.java b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenRealmUnitTest.java index 66fee0e5a9767..4eb098c0394b4 100644 --- a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenRealmUnitTest.java +++ b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenRealmUnitTest.java @@ -11,11 +11,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import io.quarkus.security.credential.TokenCredential; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; import io.quarkus.security.runtime.AnonymousIdentityProvider; import io.quarkus.security.runtime.QuarkusIdentityProviderManagerImpl; +import io.quarkus.smallrye.jwt.runtime.auth.JsonWebTokenCredential; import io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator; import io.smallrye.jwt.auth.principal.DefaultJWTParser; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; @@ -43,7 +43,7 @@ public void execute(Runnable command) { .addProvider(jwtValidator).build(); String jwt = TokenUtils.generateTokenString("/Token1.json", pk1Priv, "testTokenRealm"); - TokenAuthenticationRequest tokenEvidence = new TokenAuthenticationRequest(new TokenCredential(jwt, "bearer")); + TokenAuthenticationRequest tokenEvidence = new TokenAuthenticationRequest(new JsonWebTokenCredential(jwt)); SecurityIdentity securityIdentity = authenticator.authenticate(tokenEvidence).await().indefinitely(); Assertions.assertNotNull(securityIdentity); Assertions.assertEquals("jdoe@example.com", securityIdentity.getPrincipal().getName()); diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java index e59fe2486b337..92293c7ae94f6 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/JWTAuthMechanism.java @@ -19,6 +19,7 @@ import io.quarkus.vertx.http.runtime.security.ChallengeData; import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.jwt.auth.AbstractBearerTokenExtractor; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; import io.smallrye.mutiny.Uni; @@ -42,8 +43,10 @@ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { String jwtToken = new VertxBearerTokenExtractor(authContextInfo, context).getBearerToken(); if (jwtToken != null) { + context.put(HttpAuthenticationMechanism.class.getName(), this); return identityProviderManager - .authenticate(new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken))); + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)), context)); } return Uni.createFrom().optional(Optional.empty()); } @@ -53,7 +56,7 @@ public Uni getChallenge(RoutingContext context) { ChallengeData result = new ChallengeData( HttpResponseStatus.UNAUTHORIZED.code(), HttpHeaderNames.WWW_AUTHENTICATE, - "Bearer"); + BEARER); return Uni.createFrom().item(result); } @@ -93,7 +96,7 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { + public Uni getCredentialTransport(RoutingContext context) { final String tokenHeaderName = authContextInfo.getTokenHeader(); if (COOKIE_HEADER.equals(tokenHeaderName)) { String tokenCookieName = authContextInfo.getTokenCookie(); @@ -101,11 +104,12 @@ public HttpCredentialTransport getCredentialTransport() { if (tokenCookieName == null) { tokenCookieName = BEARER; } - return new HttpCredentialTransport(HttpCredentialTransport.Type.COOKIE, tokenCookieName); + return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.COOKIE, tokenCookieName)); } else if (AUTHORIZATION_HEADER.equals(tokenHeaderName)) { - return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BEARER); + return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BEARER)); } else { - return new HttpCredentialTransport(HttpCredentialTransport.Type.OTHER_HEADER, tokenHeaderName); + return Uni.createFrom() + .item(new HttpCredentialTransport(HttpCredentialTransport.Type.OTHER_HEADER, tokenHeaderName)); } } } diff --git a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java index fbf00321986af..1f6b80079fcfe 100644 --- a/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java +++ b/extensions/smallrye-jwt/runtime/src/main/java/io/quarkus/smallrye/jwt/runtime/auth/MpJwtValidator.java @@ -49,6 +49,9 @@ public Class getRequestType() { @Override public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { + if (!(request.getToken() instanceof JsonWebTokenCredential)) { + return Uni.createFrom().nullItem(); + } if (!blockingAuthentication) { return Uni.createFrom().emitter(new Consumer>() { @Override diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java index 11e6941f4a76a..9a0622b463c4f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/BasicAuthenticationMechanism.java @@ -155,6 +155,8 @@ public Uni authenticate(RoutingContext context, UsernamePasswordAuthenticationRequest credential = new UsernamePasswordAuthenticationRequest(userName, new PasswordCredential(password)); HttpSecurityUtils.setRoutingContextAttribute(credential, context); + context.put(HttpAuthenticationMechanism.class.getName(), this); + return identityProviderManager.authenticate(credential); } @@ -192,8 +194,8 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { - return new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BASIC); + public Uni getCredentialTransport(RoutingContext context) { + return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.AUTHORIZATION, BASIC)); } @Override diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java index e69482a8f6693..c49a17dbfd8bd 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/FormAuthenticationMechanism.java @@ -156,10 +156,12 @@ public Uni authenticate(RoutingContext context, if (context.normalizedPath().endsWith(postLocation) && context.request().method().equals(HttpMethod.POST)) { //we always re-auth if it is a post to the auth URL + context.put(HttpAuthenticationMechanism.class.getName(), this); return runFormAuth(context, identityProviderManager); } else { PersistentLoginManager.RestoreResult result = loginManager.restore(context); if (result != null) { + context.put(HttpAuthenticationMechanism.class.getName(), this); Uni ret = identityProviderManager .authenticate(HttpSecurityUtils .setRoutingContextAttribute(new TrustedAuthenticationRequest(result.getPrincipal()), context)); @@ -194,7 +196,7 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { - return new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM); + public Uni getCredentialTransport(RoutingContext context) { + return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.POST, postLocation, FORM)); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java index 7ab3f4d4f1ee3..e77542b83ea20 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticationMechanism.java @@ -31,11 +31,26 @@ default Uni sendChallenge(RoutingContext context) { } /** - * The credential transport, used to make sure multiple incompatible mechanisms are not installed + * The credential transport, used for finding the best candidate for authenticating and challenging when more than one + * mechanism is installed. + * and finding the best candidate for issuing a challenge when more than one mechanism is installed. * * May be null if this mechanism cannot interfere with other mechanisms */ - HttpCredentialTransport getCredentialTransport(); + @Deprecated + default HttpCredentialTransport getCredentialTransport() { + throw new UnsupportedOperationException(); + } + + /** + * The credential transport, used for finding the best candidate for authenticating and challenging when more than one + * mechanism is installed. + * + * May be null if this mechanism cannot interfere with other mechanisms + */ + default Uni getCredentialTransport(RoutingContext context) { + throw new UnsupportedOperationException(); + } class ChallengeSender implements Function { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java index 179b88147f108..d172f9cf840af 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthenticator.java @@ -3,9 +3,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -19,9 +17,7 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; import io.quarkus.security.identity.request.AuthenticationRequest; -import io.quarkus.vertx.http.runtime.security.HttpCredentialTransport.Type; import io.smallrye.mutiny.Uni; -import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.RoutingContext; /** @@ -29,7 +25,6 @@ */ @Singleton public class HttpAuthenticator { - private final IdentityProviderManager identityProviderManager; private final Instance pathMatchingPolicy; private final HttpAuthenticationMechanism[] mechanisms; @@ -71,21 +66,6 @@ public int compare(HttpAuthenticationMechanism mech1, HttpAuthenticationMechanis } }); this.mechanisms = mechanisms.toArray(new HttpAuthenticationMechanism[mechanisms.size()]); - //validate that we don't have multiple incompatible mechanisms - Map map = new HashMap<>(); - for (HttpAuthenticationMechanism i : mechanisms) { - HttpCredentialTransport credentialTransport = i.getCredentialTransport(); - if (credentialTransport == null) { - continue; - } - HttpAuthenticationMechanism existing = map.get(credentialTransport); - if (existing != null) { - throw new RuntimeException("Multiple mechanisms present that use the same credential transport " - + credentialTransport + ". Mechanisms are " + i + " and " + existing); - } - map.put(credentialTransport, i); - } - } } @@ -108,14 +88,29 @@ public Uni attemptAuthentication(RoutingContext routingContext String pathSpecificMechanism = pathMatchingPolicy.isResolvable() ? pathMatchingPolicy.get().getAuthMechanismName(routingContext) : null; - HttpAuthenticationMechanism matchingMech = findBestCandidateMechanism(routingContext, pathSpecificMechanism); - if (matchingMech != null) { - routingContext.put(HttpAuthenticationMechanism.class.getName(), matchingMech); - return matchingMech.authenticate(routingContext, identityProviderManager); - } else if (pathSpecificMechanism != null) { - return Uni.createFrom().optional(Optional.empty()); + Uni matchingMechUni = findBestCandidateMechanism(routingContext, pathSpecificMechanism); + if (matchingMechUni == null) { + return createSecurityIdentity(routingContext); } + return matchingMechUni.onItem() + .transformToUni(new Function>() { + + @Override + public Uni apply(HttpAuthenticationMechanism mech) { + if (mech != null) { + return mech.authenticate(routingContext, identityProviderManager); + } else if (pathSpecificMechanism != null) { + return Uni.createFrom().optional(Optional.empty()); + } + return createSecurityIdentity(routingContext); + } + + }); + + } + + private Uni createSecurityIdentity(RoutingContext routingContext) { Uni result = mechanisms[0].authenticate(routingContext, identityProviderManager); for (int i = 1; i < mechanisms.length; ++i) { HttpAuthenticationMechanism mech = mechanisms[i]; @@ -129,7 +124,6 @@ public Uni apply(SecurityIdentity data) { } }); } - return result; } @@ -196,39 +190,51 @@ public Uni apply(ChallengeData data) { return result; } - private HttpAuthenticationMechanism findBestCandidateMechanism(RoutingContext routingContext, + private Uni findBestCandidateMechanism(RoutingContext routingContext, String pathSpecificMechanism) { + Uni result = null; + if (pathSpecificMechanism != null) { - for (int i = 0; i < mechanisms.length; ++i) { - HttpCredentialTransport credType = mechanisms[i].getCredentialTransport(); - if (credType != null && credType.getAuthenticationScheme().equalsIgnoreCase(pathSpecificMechanism)) { - return mechanisms[i]; - } - } - } else { - String authScheme = getAuthorizationScheme(routingContext); - if (authScheme != null) { - for (int i = 0; i < mechanisms.length; ++i) { - HttpCredentialTransport credType = mechanisms[i].getCredentialTransport(); - if (credType != null && credType.getTransportType() == Type.AUTHORIZATION - && credType.getTypeTarget().toLowerCase().startsWith(authScheme.toLowerCase())) { - return mechanisms[i]; - } - } + result = getPathSpecificMechanism(0, routingContext, pathSpecificMechanism); + for (int i = 1; i < mechanisms.length; ++i) { + int mechIndex = i; + result = result.onItem().transformToUni( + new Function>() { + @Override + public Uni apply(HttpAuthenticationMechanism mech) { + if (mech != null) { + return Uni.createFrom().item(mech); + } + return getPathSpecificMechanism(mechIndex, routingContext, pathSpecificMechanism); + } + }); } } - return null; + return result; } - private static String getAuthorizationScheme(RoutingContext routingContext) { - String authorization = routingContext.request().getHeader(HttpHeaders.AUTHORIZATION); - if (authorization != null) { - int spaceIndex = authorization.indexOf(' '); - if (spaceIndex > 0) { - return authorization.substring(0, spaceIndex); - } + private Uni getPathSpecificMechanism(int index, RoutingContext routingContext, + String pathSpecificMechanism) { + return getCredentialTransport(mechanisms[index], routingContext).onItem() + .transform(new Function() { + @Override + public HttpAuthenticationMechanism apply(HttpCredentialTransport t) { + if (t != null && t.getAuthenticationScheme().equalsIgnoreCase(pathSpecificMechanism)) { + routingContext.put(HttpAuthenticationMechanism.class.getName(), mechanisms[index]); + return mechanisms[index]; + } + return null; + } + }); + } + + private static Uni getCredentialTransport(HttpAuthenticationMechanism mechanism, + RoutingContext routingContext) { + try { + return mechanism.getCredentialTransport(routingContext); + } catch (UnsupportedOperationException ex) { + return Uni.createFrom().item(mechanism.getCredentialTransport()); } - return null; } static class NoAuthenticationMechanism implements HttpAuthenticationMechanism { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java index cd3ba29798c9a..ebd9c4b46f046 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpCredentialTransport.java @@ -9,8 +9,9 @@ * Authorization header * POST * - * It is not permitted for multiple HTTP authentication mechanisms to use the same credential - * transport type, as they will not be able to figure out which mechanisms should process which + * Not that using multiple HTTP authentication mechanisms to use the same credential + * transport type can lead to unexpected authentication failures as they will not be able to figure out which mechanisms should + * process which * request. */ public class HttpCredentialTransport { @@ -49,7 +50,11 @@ public enum Type { /** * X509 */ - X509 + X509, + /** + * Authorizatiob code, type target is the query 'code' parameter + */ + AUTHORIZATION_CODE } @Override diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java index 5fd96c7c83a77..32ad48bdf07fe 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/MtlsAuthenticationMechanism.java @@ -55,7 +55,7 @@ public Uni authenticate(RoutingContext context, } catch (SSLPeerUnverifiedException e) { return Uni.createFrom().nullItem(); } - + context.put(HttpAuthenticationMechanism.class.getName(), this); return identityProviderManager .authenticate(HttpSecurityUtils.setRoutingContextAttribute(new CertificateAuthenticationRequest( new CertificateCredential(X509Certificate.class.cast(certificate))), context)); @@ -73,7 +73,7 @@ public Set> getCredentialTypes() { } @Override - public HttpCredentialTransport getCredentialTransport() { - return new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509"); + public Uni getCredentialTransport(RoutingContext context) { + return Uni.createFrom().item(new HttpCredentialTransport(HttpCredentialTransport.Type.X509, "X509")); } } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index e0835005f57f4..296b9ecb04113 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -210,6 +210,7 @@ oidc-client-wiremock oidc-token-propagation oidc-token-propagation-reactive + smallrye-jwt-oidc-webapp smallrye-jwt-token-propagation oidc-code-flow oidc-tenancy diff --git a/integration-tests/smallrye-jwt-oidc-webapp/pom.xml b/integration-tests/smallrye-jwt-oidc-webapp/pom.xml new file mode 100644 index 0000000000000..35c0732dcb1ae --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/pom.xml @@ -0,0 +1,262 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-smallrye-jwt-oidc-webapp + Quarkus - Integration Tests - Smallrye JWT OIDC WebApp + Module that tests that Smallrye JWT and OIDC WebApp authentication can be combined + + + http://localhost:8180/auth + + + + + org.keycloak + keycloak-adapter-core + + + org.keycloak + keycloak-core + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-oidc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-smallrye-jwt + + + io.quarkus + quarkus-smallrye-jwt-deployment + ${project.version} + pom + test + + + * + * + + + + + + net.sourceforge.htmlunit + htmlunit + test + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.url} + + + + + maven-failsafe-plugin + + false + + ${keycloak.url} + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + docker-keycloak + + + start-containers + + + + http://localhost:8180/auth + + + + + io.fabric8 + docker-maven-plugin + + + + ${keycloak.docker.image} + quarkus-test-keycloak + + + 8180:8080 + + + admin + admin + + + Keycloak: + default + cyan + + + + + http://localhost:8180 + + + + + + + true + + + + docker-start + compile + + stop + start + + + + docker-stop + post-integration-test + + stop + + + + + + org.codehaus.mojo + exec-maven-plugin + + + docker-prune + generate-resources + + exec + + + ${basedir}/../../.github/docker-prune.sh + + + + + + + + + + + diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java new file mode 100644 index 0000000000000..97d8c268ef160 --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -0,0 +1,23 @@ +package io.quarkus.it.keycloak; + +import javax.annotation.security.RolesAllowed; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; + +@Path("/protected") +@Authenticated +public class ProtectedResource { + + @Inject + SecurityIdentity identity; + + @GET + @RolesAllowed("user") + public String principalName() { + return identity.getPrincipal().getName(); + } +} diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties b/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties new file mode 100644 index 0000000000000..117d7a88842f9 --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties @@ -0,0 +1,13 @@ +mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs +mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus +smallrye.jwt.path.groups=realm_access/roles + +mp.jwt.token.header=Cookie +smallrye.jwt.always-check-authorization=true + +quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus +quarkus.oidc.application-type=web-app +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.secret=secret + + diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java new file mode 100644 index 0000000000000..82cdec1837c5e --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -0,0 +1,144 @@ +package io.quarkus.it.keycloak; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.RolesRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.util.JsonSerialization; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.restassured.RestAssured; + +public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { + + private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); + private static final String KEYCLOAK_REALM = "quarkus"; + + @Override + public Map start() { + + RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + realm.setRevokeRefreshToken(true); + realm.setRefreshTokenMaxReuse(0); + realm.setAccessTokenLifespan(3); + + realm.getClients().add(createClient("quarkus-app")); + realm.getUsers().add(createUser("alice", "user")); + realm.getUsers().add(createUser("bob", "user")); + realm.getUsers().add(createUser("john", "tester")); + + try { + RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .contentType("application/json") + .body(JsonSerialization.writeValueAsBytes(realm)) + .when() + .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() + .statusCode(201); + } catch (IOException e) { + throw new RuntimeException(e); + } + return Collections.emptyMap(); + } + + private static String getAdminAccessToken() { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", "admin") + .param("password", "admin") + .param("client_id", "admin-cli") + .when() + .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } + + private static RealmRepresentation createRealm(String name) { + RealmRepresentation realm = new RealmRepresentation(); + + realm.setRealm(name); + realm.setEnabled(true); + realm.setUsers(new ArrayList<>()); + realm.setClients(new ArrayList<>()); + realm.setAccessTokenLifespan(3); + realm.setSsoSessionMaxLifespan(3); + RolesRepresentation roles = new RolesRepresentation(); + List realmRoles = new ArrayList<>(); + + roles.setRealm(realmRoles); + realm.setRoles(roles); + + realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); + realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); + + return realm; + } + + private static ClientRepresentation createClient(String clientId) { + ClientRepresentation client = new ClientRepresentation(); + + client.setClientId(clientId); + client.setPublicClient(false); + client.setSecret("secret"); + client.setDirectAccessGrantsEnabled(true); + client.setServiceAccountsEnabled(true); + client.setRedirectUris(Arrays.asList("*")); + client.setEnabled(true); + client.setDefaultClientScopes(List.of("microprofile-jwt")); + return client; + } + + private static UserRepresentation createUser(String username, String... realmRoles) { + UserRepresentation user = new UserRepresentation(); + + user.setUsername(username); + user.setEnabled(true); + user.setCredentials(new ArrayList<>()); + user.setRealmRoles(Arrays.asList(realmRoles)); + user.setEmail(username + "@gmail.com"); + + CredentialRepresentation credential = new CredentialRepresentation(); + + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(username); + credential.setTemporary(false); + + user.getCredentials().add(credential); + + return user; + } + + @Override + public void stop() { + + RestAssured + .given() + .auth().oauth2(getAdminAccessToken()) + .when() + .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); + } + + public static String getAccessToken(String userName) { + return RestAssured + .given() + .param("grant_type", "password") + .param("username", userName) + .param("password", userName) + .param("client_id", "quarkus-app") + .param("client_secret", "secret") + .when() + .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") + .as(AccessTokenResponse.class).getToken(); + } +} diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppInGraalITCase.java b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppInGraalITCase.java new file mode 100644 index 0000000000000..88f0505ce304b --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppInGraalITCase.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class SmallRyeJwtOidcWebAppInGraalITCase extends SmallRyeJwtOidcWebAppTest { +} diff --git a/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java new file mode 100644 index 0000000000000..ac0f1e81522da --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java @@ -0,0 +1,88 @@ +package io.quarkus.it.keycloak; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +@QuarkusTestResource(KeycloakRealmResourceManager.class) +public class SmallRyeJwtOidcWebAppTest { + + @Test + public void testGetUserNameWithBearerToken() { + RestAssured.given().auth().oauth2(KeycloakRealmResourceManager.getAccessToken("alice")) + .when().get("/protected") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + + @Test + public void testGetUserNameWithWrongBearerToken() { + RestAssured.given().auth().oauth2("123") + .when().get("/protected") + .then() + .statusCode(401); + } + + @Test + public void testGetUserNameWithCookieToken() { + RestAssured.given().header("Cookie", "Bearer=" + KeycloakRealmResourceManager.getAccessToken("alice")) + .when().get("/protected") + .then() + .statusCode(200) + .body(equalTo("alice")); + } + + @Test + public void testGetUserNameWithWrongCookieToken() { + RestAssured.given().header("Cookie", "Bearer=123") + .when().get("/protected") + .then() + .statusCode(401); + } + + @Test + public void testNoToken() { + // OIDC has a higher priority than JWT + RestAssured.given().when().redirects().follow(false) + .get("/protected") + .then() + .statusCode(302); + } + + @Test + public void testGetUserNameWithCodeFlow() throws Exception { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + page = loginForm.getInputByName("login").click(); + + assertEquals("alice", page.getBody().asText()); + webClient.getCookieManager().clearCookies(); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } +}