Skip to content

Commit

Permalink
Allow token verification with user info when no introspection endpoint
Browse files Browse the repository at this point in the history
closes: #20911
  • Loading branch information
michalvavrik committed Dec 6, 2022
1 parent c8bd1ba commit afab697
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect-providers.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ quarkus.oidc.client-id=<Client ID>
quarkus.oidc.credentials.secret=<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]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,15 @@ public static enum ApplicationType {
@ConfigItem
public Optional<Provider> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {

private Uni<SecurityIdentity> createSecurityIdentityWithOidcServer(RoutingContext vertxContext,
TokenAuthenticationRequest request, TenantConfigContext resolvedContext, final UserInfo userInfo) {
Uni<TokenVerificationResult> tokenUni = null;
final Uni<TokenVerificationResult> 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
Expand All @@ -148,7 +148,17 @@ private Uni<SecurityIdentity> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class OidcProvider implements Closeable {
final String[] audience;
final Map<String, String> requiredClaims;
final Key tokenDecryptionKey;
final boolean verifyUserAuthSuccessWithUserInfo;

public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) {
this.client = client;
Expand All @@ -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) {
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,23 @@ private Uni<TenantConfigContext> 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));
}
Expand Down
17 changes: 17 additions & 0 deletions integration-tests/oidc-wiremock/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
Expand Down Expand Up @@ -77,6 +81,19 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +25,10 @@ public class CodeFlowUserInfoResource {
@Inject
DefaultTokenIntrospectionUserInfoCache tokenCache;

@RestClient
@Inject
TokenPropagatingOidcClient tokenPropagatingOidcClient;

@GET
@Path("/code-flow-user-info-only")
public String access() {
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public Uni<SecurityIdentity> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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=/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit afab697

Please sign in to comment.