diff --git a/docs/src/main/asciidoc/security-openid-connect-providers.adoc b/docs/src/main/asciidoc/security-openid-connect-providers.adoc index 0672a7b2896c59..65b53ee7111717 100644 --- a/docs/src/main/asciidoc/security-openid-connect-providers.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-providers.adoc @@ -49,6 +49,8 @@ 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` 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]: 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/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/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..a4e8d1dad5638c 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -72,6 +72,16 @@ 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.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.authorization-path=/ 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");