From c9776184ac4414dac1d6ff49fdb1639374939576 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Wed, 22 Dec 2021 16:47:23 +0000 Subject: [PATCH] Resolve HttpCredentialTransport dynamically --- .../runtime/OidcAuthenticationMechanism.java | 23 +- .../oidc/runtime/OidcIdentityProvider.java | 3 + .../jwt/runtime/auth/JWTAuthMechanism.java | 15 +- .../jwt/runtime/auth/MpJwtValidator.java | 6 + .../BasicAuthenticationMechanism.java | 4 +- .../security/FormAuthenticationMechanism.java | 4 +- .../security/HttpAuthenticationMechanism.java | 19 +- .../runtime/security/HttpAuthenticator.java | 128 ++++++--- .../security/HttpCredentialTransport.java | 11 +- .../security/MtlsAuthenticationMechanism.java | 4 +- integration-tests/pom.xml | 1 + .../smallrye-jwt-oidc-webapp/pom.xml | 262 ++++++++++++++++++ .../it/keycloak/ProtectedResource.java | 23 ++ .../src/main/resources/application.properties | 10 + .../KeycloakRealmResourceManager.java | 144 ++++++++++ .../SmallRyeJwtOidcWebAppInGraalITCase.java | 7 + .../keycloak/SmallRyeJwtOidcWebAppTest.java | 62 +++++ 17 files changed, 668 insertions(+), 58 deletions(-) create mode 100644 integration-tests/smallrye-jwt-oidc-webapp/pom.xml create mode 100644 integration-tests/smallrye-jwt-oidc-webapp/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java create mode 100644 integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties create mode 100644 integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java create mode 100644 integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppInGraalITCase.java create mode 100644 integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java 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 26a97a7a8b64d8..899b6bd792e689 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 @@ -9,6 +9,7 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; @@ -21,10 +22,13 @@ @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) { @@ -43,6 +47,7 @@ public Uni apply(OidcTenantConfig oidcConfig) { if (!oidcConfig.tenantEnabled) { return Uni.createFrom().nullItem(); } + context.put(IdentityProvider.class.getName(), OidcIdentityProvider.class.getName()); return isWebApp(context, oidcConfig) ? codeAuth.authenticate(context, identityProviderManager, oidcConfig) : bearerAuth.authenticate(context, identityProviderManager, oidcConfig); } @@ -89,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) { 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 b34d04fbc6236e..7e2148b7fdd275 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 @@ -59,6 +59,9 @@ public Class getRequestType() { public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { RoutingContext vertxContext = HttpSecurityUtils.getRoutingContextAttribute(request); + if (!this.getClass().getName().equals(vertxContext.get(IdentityProvider.class.getName()))) { + return Uni.createFrom().nullItem(); + } vertxContext.put(AuthenticationRequestContext.class.getName(), context); Uni tenantConfigContext = tenantResolver.resolveContext(vertxContext); 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 e59fe2486b337e..a9af1c648f27e4 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 @@ -12,6 +12,7 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AuthenticationRequest; @@ -19,6 +20,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 +44,10 @@ public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { String jwtToken = new VertxBearerTokenExtractor(authContextInfo, context).getBearerToken(); if (jwtToken != null) { + context.put(IdentityProvider.class.getName(), MpJwtValidator.class.getName()); return identityProviderManager - .authenticate(new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken))); + .authenticate(HttpSecurityUtils.setRoutingContextAttribute( + new TokenAuthenticationRequest(new JsonWebTokenCredential(jwtToken)), context)); } return Uni.createFrom().optional(Optional.empty()); } @@ -93,7 +97,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 +105,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 fbf00321986af7..c79b5a61274b60 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 @@ -14,10 +14,12 @@ import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils; import io.smallrye.jwt.auth.principal.JWTParser; import io.smallrye.jwt.auth.principal.ParseException; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.subscription.UniEmitter; +import io.vertx.ext.web.RoutingContext; /** * Validates a bearer token according to the MP-JWT rules @@ -49,6 +51,10 @@ public Class getRequestType() { @Override public Uni authenticate(TokenAuthenticationRequest request, AuthenticationRequestContext context) { + RoutingContext vertxContext = HttpSecurityUtils.getRoutingContextAttribute(request); + if (!this.getClass().getName().equals(vertxContext.get(IdentityProvider.class.getName()))) { + 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 11e6941f4a76a3..257c7d61e52267 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 @@ -192,8 +192,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 e69482a8f6693d..7fe665c992ed6d 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 @@ -194,7 +194,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 7ab3f4d4f1ee35..e77542b83ea209 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 179b88147f1083..a5ab20989dc718 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; @@ -29,6 +27,7 @@ */ @Singleton public class HttpAuthenticator { + private static final Uni NULL_UNI = Uni.createFrom().nullItem(); private final IdentityProviderManager identityProviderManager; private final Instance pathMatchingPolicy; @@ -71,21 +70,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 +92,30 @@ 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) { + routingContext.put(HttpAuthenticationMechanism.class.getName(), mech); + 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 +129,6 @@ public Uni apply(SecurityIdentity data) { } }); } - return result; } @@ -196,28 +195,83 @@ 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]; - } + 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); + } + }); } } 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 = getAuthorizationSchemeMechanism(0, routingContext, authScheme); + 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 getAuthorizationSchemeMechanism(mechIndex, routingContext, authScheme); + } + }); } } } - return null; + return result; + } + + 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)) { + return mechanisms[index]; + } + return null; + } + }); + } + + private Uni getAuthorizationSchemeMechanism(int index, RoutingContext routingContext, + String authScheme) { + return getCredentialTransport(mechanisms[index], routingContext).onItem() + .transform(new Function() { + @Override + public HttpAuthenticationMechanism apply(HttpCredentialTransport t) { + if (t != null && t.getTransportType() == Type.AUTHORIZATION + && t.getTypeTarget().toLowerCase().startsWith(authScheme.toLowerCase())) { + 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()); + } } private static String getAuthorizationScheme(RoutingContext routingContext) { 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 cd3ba29798c9a0..ebd9c4b46f0468 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 5fd96c7c83a77c..9c0ab1be8300d4 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 @@ -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 57520c4e716a02..867cf7e3abf077 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -207,6 +207,7 @@ oidc-client-reactive oidc-client-wiremock oidc-token-propagation + 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 00000000000000..35c0732dcb1ae7 --- /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 00000000000000..97d8c268ef1604 --- /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 00000000000000..82d01a7339aafe --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/main/resources/application.properties @@ -0,0 +1,10 @@ +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 + +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 00000000000000..82cdec1837c5e6 --- /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 00000000000000..88f0505ce304b9 --- /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 00000000000000..7e3ed5f4c1f387 --- /dev/null +++ b/integration-tests/smallrye-jwt-oidc-webapp/src/test/java/io/quarkus/it/keycloak/SmallRyeJwtOidcWebAppTest.java @@ -0,0 +1,62 @@ +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 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; + } +}