From a292ef770e0611ebb2ef78b4d78ff449b08bb17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Tue, 6 Dec 2022 23:09:26 +0100 Subject: [PATCH] Allow token verification with user info when no introspection endpoint closes: #20911 --- .../security-openid-connect-providers.adoc | 9 ++++ ...ity-openid-connect-web-authentication.adoc | 3 ++ .../io/quarkus/oidc/OidcTenantConfig.java | 9 ++++ .../oidc/runtime/OidcIdentityProvider.java | 14 +++++- .../io/quarkus/oidc/runtime/OidcProvider.java | 3 ++ .../io/quarkus/oidc/runtime/OidcRecorder.java | 17 +++++++ .../io/quarkus/oidc/runtime/OidcUtils.java | 3 +- .../runtime/providers/KnownOidcProviders.java | 45 ++++++++++--------- .../quarkus/oidc/runtime/OidcUtilsTest.java | 42 +++++++++++------ integration-tests/oidc-wiremock/pom.xml | 17 +++++++ .../it/keycloak/CodeFlowUserInfoResource.java | 21 ++++++++- .../CustomSecurityIdentityAugmentor.java | 1 + .../keycloak/CustomTenantConfigResolver.java | 1 + .../it/keycloak/CustomTenantResolver.java | 3 ++ .../OidcClientRequestCustomFilter.java | 24 ++++++++++ .../keycloak/TokenPropagatingOidcClient.java | 18 ++++++++ .../src/main/resources/application.properties | 12 +++++ .../keycloak/CodeFlowAuthorizationTest.java | 10 ++++- 18 files changed, 211 insertions(+), 41 deletions(-) create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcClientRequestCustomFilter.java create mode 100644 integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenPropagatingOidcClient.java diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 0672a7b2896c59..97aea1c5ea3e1b 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -45,10 +45,13 @@ Now add the following configuration to your `application.properties`: [source,properties] ---- quarkus.oidc.provider=github +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- +TIP: You can also use GitHub provider with `quarkus.oidc.application-type=service`, just set configuration property `quarkus.oidc.allow-bearer-token-verification-with-user-info=true` to true. + === Google In order to set up OIDC for Google you need to create a new project in your https://console.cloud.google.com/projectcreate[Google Cloud Platform console]: @@ -95,6 +98,7 @@ You can now configure your `application.properties`: [source,properties] ---- quarkus.oidc.provider=google +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- @@ -136,6 +140,7 @@ You can now configure your `application.properties`: [source,properties] ---- quarkus.oidc.provider=microsoft +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- @@ -239,6 +244,7 @@ You can now configure your `application.properties`: [source,properties] ---- quarkus.oidc.provider=apple +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.jwt.key-file=AuthKey_.p8 quarkus.oidc.credentials.jwt.token-key-id= @@ -281,6 +287,7 @@ You can now configure your `application.properties`: [source,properties] ---- quarkus.oidc.provider=facebook +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- @@ -337,6 +344,7 @@ You can now configure your `application.properties`: [source,properties] ---- quarkus.oidc.provider=twitter +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- @@ -356,6 +364,7 @@ You can now configure your `application.properties`: [source,properties] ---- quarkus.oidc.provider=spotify +quarkus.oidc.application-type=web-app quarkus.oidc.client-id= quarkus.oidc.credentials.secret= ---- diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 3fd5893ef29f14..239578381b7d42 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -767,6 +767,7 @@ Here is how you can integrate `quarkus-oidc` with GitHub after you have link:htt [source,properties] ---- quarkus.oidc.provider=github +quarkus.oidc.application-type=web-app quarkus.oidc.client-id=github_app_clientid quarkus.oidc.credentials.secret=github_app_clientsecret @@ -923,6 +924,7 @@ and configure Google OIDC properties: [source, properties] ---- quarkus.oidc.provider=google +quarkus.oidc.application-type=web-app quarkus.oidc.client-id={GOOGLE_CLIENT_ID} quarkus.oidc.credentials.secret={GOOGLE_CLIENT_SECRET} quarkus.oidc.token.issuer=https://accounts.google.com @@ -1106,6 +1108,7 @@ Apple OpenID Connect Provider uses a `client_secret_post` method where a secret ---- # Apple provider configuration sets a 'client_secret_post_jwt' authentication method quarkus.oidc.provider=apple +quarkus.oidc.application-type=web-app quarkus.oidc.client-id=${apple.client-id} quarkus.oidc.credentials.jwt.key-file=ecPrivateKey.pem diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index b8a69106aea5d9..bf6835c7e55712 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -1338,6 +1338,15 @@ public static enum ApplicationType { @ConfigItem public Optional provider = Optional.empty(); + /** + * Indirectly verify the user authentication has been successful with request to `user-info-path`. + * Bearer token is considered valid if `user-info-path` endpoint responded with `200 OK`. + * You should only enable this option with OIDC non-compliant provider without introspection endpoint. + * Please make sure 'user-info' endpoint is secured. + */ + @ConfigItem(defaultValue = "false") + public boolean allowBearerTokenVerificationWithUserInfo; + public static enum Provider { APPLE, FACEBOOK, 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 91a9ece1300678..ee4965a57f4451 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 @@ -138,7 +138,7 @@ public Uni apply(UserInfo userInfo, Throwable t) { private Uni createSecurityIdentityWithOidcServer(RoutingContext vertxContext, TokenAuthenticationRequest request, TenantConfigContext resolvedContext, final UserInfo userInfo) { - Uni tokenUni = null; + final Uni tokenUni; if (isInternalIdToken(request)) { if (vertxContext.get(NEW_AUTHENTICATION) == Boolean.TRUE) { // No need to verify it in this case as 'CodeAuthenticationMechanism' has just created it @@ -148,7 +148,17 @@ private Uni createSecurityIdentityWithOidcServer(RoutingContex tokenUni = verifySelfSignedTokenUni(resolvedContext, request.getToken().getToken()); } } else { - tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken()); + if (resolvedContext.provider.verifyUserAuthSuccessWithUserInfo) { + if (userInfo == null) { + tokenUni = Uni.createFrom().failure( + new AuthenticationFailedException("Bearer token verification failed as user info is null.")); + } else { + // valid token verification result with empty JWT content + tokenUni = Uni.createFrom().item(new TokenVerificationResult(new JsonObject(), null)); + } + } else { + tokenUni = verifyTokenUni(resolvedContext, request.getToken().getToken()); + } } return tokenUni.onItemOrFailure() diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index b61825efa79636..daaaf485ed4e68 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -59,6 +59,7 @@ public class OidcProvider implements Closeable { final String[] audience; final Map requiredClaims; final Key tokenDecryptionKey; + final boolean verifyUserAuthSuccessWithUserInfo; public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) { this.client = client; @@ -70,6 +71,7 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.audience = checkAudienceProp(); this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; + this.verifyUserAuthSuccessWithUserInfo = oidcConfig != null && oidcConfig.allowBearerTokenVerificationWithUserInfo; } public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) { @@ -80,6 +82,7 @@ public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenD this.audience = checkAudienceProp(); this.requiredClaims = checkRequiredClaimsProp(); this.tokenDecryptionKey = tokenDecryptionKey; + this.verifyUserAuthSuccessWithUserInfo = oidcConfig != null && oidcConfig.allowBearerTokenVerificationWithUserInfo; } private String checkIssuerProp() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index d0db470486f0d6..35a87876646683 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -214,6 +214,23 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf } } + if (oidcConfig.allowBearerTokenVerificationWithUserInfo) { + if (!oidcConfig.authentication.isUserInfoRequired().orElse(false)) { + throw new ConfigurationException( + "UserInfo is not required but 'allowBearerTokenVerificationWithUserInfo' is enabled"); + } + if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { + if (oidcConfig.userInfoPath.isEmpty()) { + throw new ConfigurationException( + "UserInfo path is missing but 'allowBearerTokenVerificationWithUserInfo' is enabled"); + } + if (oidcConfig.introspectionPath.isPresent()) { + throw new ConfigurationException( + "Introspection path is configured and 'allowBearerTokenVerificationWithUserInfo' is enabled, these options are mutually exclusive"); + } + } + } + return createOidcProvider(oidcConfig, tlsConfig, vertx) .onItem().transform(p -> new TenantConfigContext(p, oidcConfig)); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index a9b245bf4e1da0..f1adb0ff130dc1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -436,7 +436,8 @@ static OidcTenantConfig mergeTenantConfig(OidcTenantConfig tenant, OidcTenantCon static OidcTenantConfig resolveProviderConfig(OidcTenantConfig oidcTenantConfig) { if (oidcTenantConfig != null && oidcTenantConfig.provider.isPresent()) { return OidcUtils.mergeTenantConfig(oidcTenantConfig, - KnownOidcProviders.provider(oidcTenantConfig.provider.get())); + KnownOidcProviders.provider(oidcTenantConfig.provider.get(), + oidcTenantConfig.applicationType.orElse(OidcTenantConfig.ApplicationType.SERVICE))); } else { return oidcTenantConfig; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java index 6c94a35a371539..7751bbcb74414e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java @@ -9,29 +9,30 @@ public class KnownOidcProviders { - public static OidcTenantConfig provider(OidcTenantConfig.Provider provider) { + public static OidcTenantConfig provider(OidcTenantConfig.Provider provider, + OidcTenantConfig.ApplicationType applicationType) { if (OidcTenantConfig.Provider.GITHUB == provider) { - return github(); + return github(applicationType); } else if (OidcTenantConfig.Provider.GOOGLE == provider) { - return google(); + return google(applicationType); } else if (OidcTenantConfig.Provider.APPLE == provider) { - return apple(); + return apple(applicationType); } else if (OidcTenantConfig.Provider.MICROSOFT == provider) { - return microsoft(); + return microsoft(applicationType); } else if (OidcTenantConfig.Provider.FACEBOOK == provider) { - return facebook(); + return facebook(applicationType); } else if (OidcTenantConfig.Provider.SPOTIFY == provider) { - return spotify(); + return spotify(applicationType); } else if (OidcTenantConfig.Provider.TWITTER == provider) { - return twitter(); + return twitter(applicationType); } return null; } - private static OidcTenantConfig github() { + private static OidcTenantConfig github(OidcTenantConfig.ApplicationType applicationType) { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://github.com/login/oauth"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.setDiscoveryEnabled(false); ret.setAuthorizationPath("authorize"); ret.setTokenPath("access_token"); @@ -42,10 +43,10 @@ private static OidcTenantConfig github() { return ret; } - private static OidcTenantConfig twitter() { + private static OidcTenantConfig twitter(OidcTenantConfig.ApplicationType applicationType) { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://api.twitter.com/2/oauth2"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.setDiscoveryEnabled(false); ret.setAuthorizationPath("https://twitter.com/i/oauth2/authorize"); ret.setTokenPath("token"); @@ -58,27 +59,27 @@ private static OidcTenantConfig twitter() { return ret; } - private static OidcTenantConfig google() { + private static OidcTenantConfig google(OidcTenantConfig.ApplicationType applicationType) { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://accounts.google.com"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.getAuthentication().setScopes(List.of("openid", "email", "profile")); return ret; } - private static OidcTenantConfig microsoft() { + private static OidcTenantConfig microsoft(OidcTenantConfig.ApplicationType applicationType) { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://login.microsoftonline.com/common/v2.0"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.getToken().setIssuer("any"); ret.getAuthentication().setScopes(List.of("openid", "email", "profile")); return ret; } - private static OidcTenantConfig facebook() { + private static OidcTenantConfig facebook(OidcTenantConfig.ApplicationType applicationType) { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://www.facebook.com"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.setDiscoveryEnabled(false); ret.setAuthorizationPath("https://facebook.com/dialog/oauth/"); ret.setTokenPath("https://graph.facebook.com/v12.0/oauth/access_token"); @@ -88,10 +89,10 @@ private static OidcTenantConfig facebook() { return ret; } - private static OidcTenantConfig apple() { + private static OidcTenantConfig apple(OidcTenantConfig.ApplicationType applicationType) { OidcTenantConfig ret = new OidcTenantConfig(); ret.setAuthServerUrl("https://appleid.apple.com/"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.getAuthentication().setScopes(List.of("openid", "email", "name")); ret.getAuthentication().setForceRedirectHttpsScheme(true); ret.getAuthentication().setResponseMode(ResponseMode.FORM_POST); @@ -101,12 +102,12 @@ private static OidcTenantConfig apple() { return ret; } - private static OidcTenantConfig spotify() { + private static OidcTenantConfig spotify(OidcTenantConfig.ApplicationType applicationType) { // See https://developer.spotify.com/documentation/general/guides/authorization/code-flow/ OidcTenantConfig ret = new OidcTenantConfig(); ret.setDiscoveryEnabled(false); ret.setAuthServerUrl("https://accounts.spotify.com"); - ret.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); + ret.setApplicationType(applicationType); ret.setAuthorizationPath("authorize"); ret.setTokenPath("api/token"); ret.setUserInfoPath("https://api.spotify.com/v1/me"); diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java index 37cc57d5294e9b..ca17507751a17f 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcUtilsTest.java @@ -37,7 +37,8 @@ public class OidcUtilsTest { public void testAcceptGitHubProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GITHUB)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.GITHUB, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -68,7 +69,8 @@ public void testOverrideGitHubProperties() throws Exception { tenant.authentication.setUserInfoRequired(false); tenant.authentication.setScopes(List.of("write")); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GITHUB)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.GITHUB, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); @@ -87,7 +89,8 @@ public void testOverrideGitHubProperties() throws Exception { public void testAcceptTwitterProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.TWITTER, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -122,7 +125,8 @@ public void testOverrideTwitterProperties() throws Exception { tenant.authentication.setPkceRequired(false); tenant.authentication.setScopes(List.of("write")); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.TWITTER)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.TWITTER, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); @@ -143,7 +147,8 @@ public void testOverrideTwitterProperties() throws Exception { public void testAcceptFacebookProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.FACEBOOK, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -172,7 +177,8 @@ public void testOverrideFacebookProperties() throws Exception { tenant.authentication.setScopes(List.of("write")); tenant.authentication.setForceRedirectHttpsScheme(false); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.FACEBOOK)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.FACEBOOK, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); @@ -190,7 +196,8 @@ public void testOverrideFacebookProperties() throws Exception { public void testAcceptGoogleProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.GOOGLE, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -207,7 +214,8 @@ public void testOverrideGoogleProperties() throws Exception { tenant.setAuthServerUrl("http://localhost/wiremock"); tenant.authentication.setScopes(List.of("write")); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.GOOGLE)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.GOOGLE, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); @@ -219,7 +227,8 @@ public void testOverrideGoogleProperties() throws Exception { public void testAcceptMicrosoftProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MICROSOFT)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.MICROSOFT, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -239,7 +248,8 @@ public void testOverrideMicrosoftProperties() throws Exception { tenant.authentication.setScopes(List.of("write")); tenant.authentication.setForceRedirectHttpsScheme(false); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.MICROSOFT)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.MICROSOFT, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); @@ -253,7 +263,8 @@ public void testOverrideMicrosoftProperties() throws Exception { public void testAcceptAppleProperties() throws Exception { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.APPLE)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.APPLE, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -279,7 +290,8 @@ public void testOverrideAppleProperties() throws Exception { tenant.credentials.jwt.setAudience("http://localhost/audience"); tenant.credentials.jwt.setSignatureAlgorithm(SignatureAlgorithm.ES256.getAlgorithm()); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.APPLE)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.APPLE, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); @@ -295,7 +307,8 @@ public void testOverrideAppleProperties() throws Exception { public void testAcceptSpotifyProperties() { OidcTenantConfig tenant = new OidcTenantConfig(); tenant.setTenantId(OidcUtils.DEFAULT_TENANT_ID); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.SPOTIFY)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.SPOTIFY, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.WEB_APP, config.getApplicationType().get()); @@ -314,7 +327,8 @@ public void testOverrideSpotifyProperties() { tenant.authentication.setScopes(List.of("write")); tenant.authentication.setForceRedirectHttpsScheme(false); - OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, KnownOidcProviders.provider(Provider.SPOTIFY)); + OidcTenantConfig config = OidcUtils.mergeTenantConfig(tenant, + KnownOidcProviders.provider(Provider.SPOTIFY, ApplicationType.WEB_APP)); assertEquals(OidcUtils.DEFAULT_TENANT_ID, config.getTenantId().get()); assertEquals(ApplicationType.HYBRID, config.getApplicationType().get()); diff --git a/integration-tests/oidc-wiremock/pom.xml b/integration-tests/oidc-wiremock/pom.xml index 3eb96fedbef001..1f1eb2726fc273 100644 --- a/integration-tests/oidc-wiremock/pom.xml +++ b/integration-tests/oidc-wiremock/pom.xml @@ -18,6 +18,10 @@ io.quarkus quarkus-oidc + + io.quarkus + quarkus-oidc-client-filter + io.quarkus quarkus-resteasy-jackson @@ -77,6 +81,19 @@ + + io.quarkus + quarkus-oidc-client-filter-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java index 1353ab349ff36b..f3e13d62445227 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowUserInfoResource.java @@ -3,6 +3,9 @@ import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; + +import org.eclipse.microprofile.rest.client.inject.RestClient; import io.quarkus.oidc.UserInfo; import io.quarkus.oidc.runtime.DefaultTokenIntrospectionUserInfoCache; @@ -22,6 +25,10 @@ public class CodeFlowUserInfoResource { @Inject DefaultTokenIntrospectionUserInfoCache tokenCache; + @RestClient + @Inject + TokenPropagatingOidcClient tokenPropagatingOidcClient; + @GET @Path("/code-flow-user-info-only") public String access() { @@ -33,7 +40,19 @@ public String access() { @GET @Path("/code-flow-user-info-github") - public String accessGitHub() { + public String accessGitHub(@QueryParam("propagate-bearer-token") Boolean propagateToServiceEndpoint) { + final String access; + if (Boolean.TRUE.equals(propagateToServiceEndpoint)) { + access = tokenPropagatingOidcClient.accessGitHubService(); + } else { + access = access(); + } + return access; + } + + @GET + @Path("/bearer-user-info-github-service") + public String accessGitHubService() { return access(); } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java index 5271cc2bdccae8..84eae694557c0b 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomSecurityIdentityAugmentor.java @@ -21,6 +21,7 @@ public Uni augment(SecurityIdentity identity, AuthenticationRe if (routingContext != null && (routingContext.normalizedPath().endsWith("code-flow-user-info-only") || routingContext.normalizedPath().endsWith("code-flow-user-info-github") + || routingContext.normalizedPath().endsWith("bearer-user-info-github-service") || routingContext.normalizedPath().endsWith("code-flow-user-info-dynamic-github") || routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken"))) { QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index e98125e3d182fb..edb6fd457beb0a 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -31,6 +31,7 @@ public Uni resolve(RoutingContext context, config.setTenantId("code-flow-user-info-dynamic-github"); config.setProvider(Provider.GITHUB); + config.setApplicationType(OidcTenantConfig.ApplicationType.WEB_APP); config.setAuthServerUrl(keycloakUrl + "/realms/quarkus/"); config.setAuthorizationPath("/"); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 1473b7fb802a98..61ee4ff44d3268 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -32,6 +32,9 @@ public String resolve(RoutingContext context) { if (path.endsWith("code-flow-user-info-github")) { return "code-flow-user-info-github"; } + if (path.endsWith("bearer-user-info-github-service")) { + return "bearer-user-info-github-service"; + } if (path.endsWith("code-flow-user-info-github-cached-in-idtoken")) { return "code-flow-user-info-github-cached-in-idtoken"; } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcClientRequestCustomFilter.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcClientRequestCustomFilter.java new file mode 100644 index 00000000000000..a25dbe163b1b97 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcClientRequestCustomFilter.java @@ -0,0 +1,24 @@ +package io.quarkus.it.keycloak; + +import javax.annotation.Priority; +import javax.inject.Inject; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; + +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.vertx.ext.web.RoutingContext; + +@Priority(Priorities.AUTHENTICATION) +public class OidcClientRequestCustomFilter implements ClientRequestFilter { + + @Inject + RoutingContext routingContext; + + @Override + public void filter(ClientRequestContext requestContext) { + String accessToken = routingContext.get(OidcConstants.ACCESS_TOKEN_VALUE); + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenPropagatingOidcClient.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenPropagatingOidcClient.java new file mode 100644 index 00000000000000..4a9db56445f0d0 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/TokenPropagatingOidcClient.java @@ -0,0 +1,18 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient +@RegisterProvider(OidcClientRequestCustomFilter.class) +@Path("/") +public interface TokenPropagatingOidcClient { + + @Path("/bearer-user-info-github-service") + @GET + String accessGitHubService(); + +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index ee4cc8c82d988b..6300a1dc7ce96a 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -64,6 +64,7 @@ quarkus.oidc.code-flow-user-info-only.credentials.secret=AyM1SysPpbyDfgZld3umj1q quarkus.oidc.code-flow-user-info-only.application-type=web-app quarkus.oidc.code-flow-user-info-github.provider=github +quarkus.oidc.code-flow-user-info-github.application-type=web-app quarkus.oidc.code-flow-user-info-github.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-github.authorization-path=/ quarkus.oidc.code-flow-user-info-github.user-info-path=protocol/openid-connect/userinfo @@ -72,7 +73,18 @@ quarkus.oidc.code-flow-user-info-github.code-grant.headers.X-Custom=XCustomHeade quarkus.oidc.code-flow-user-info-github.client-id=quarkus-web-app quarkus.oidc.code-flow-user-info-github.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +quarkus.oidc.bearer-user-info-github-service.provider=github +quarkus.oidc.bearer-user-info-github-service.allow-bearer-token-verification-with-user-info=true +quarkus.oidc.bearer-user-info-github-service.application-type=service +quarkus.oidc.bearer-user-info-github-service.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.bearer-user-info-github-service.authorization-path=/ +quarkus.oidc.bearer-user-info-github-service.user-info-path=protocol/openid-connect/userinfo +quarkus.oidc.bearer-user-info-github-service.client-id=quarkus-web-app +quarkus.oidc.bearer-user-info-github-service.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow +io.quarkus.it.keycloak.TokenPropagatingOidcClient/mp-rest/url=http://localhost:8081/ + quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.provider=github +quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.application-type=web-app quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authorization-path=/ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.user-info-path=protocol/openid-connect/userinfo diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index 9b574aa049a349..d8e3103ed220fd 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -198,13 +198,21 @@ public void testCodeFlowUserInfo() throws IOException { doTestCodeFlowUserInfo("code-flow-user-info-github"); doTestCodeFlowUserInfo("code-flow-user-info-dynamic-github"); + // web-app endpoint propagates bearer token to the downstream OIDC service endpoint + doTestCodeFlowUserInfo("code-flow-user-info-github", "propagate-bearer-token=true"); + doTestCodeFlowUserInfoCashedInIdToken(); } private void doTestCodeFlowUserInfo(String tenantId) throws IOException { + doTestCodeFlowUserInfo(tenantId, null); + } + + private void doTestCodeFlowUserInfo(String tenantId, String queryParam) throws IOException { try (final WebClient webClient = createWebClient()) { webClient.getOptions().setRedirectEnabled(true); - HtmlPage page = webClient.getPage("http://localhost:8081/" + tenantId); + HtmlPage page = webClient + .getPage("http://localhost:8081/" + tenantId + (queryParam == null ? "" : "?" + queryParam)); HtmlForm form = page.getFormByName("form"); form.getInputByName("username").type("alice");