Skip to content

Commit

Permalink
Accept signed OIDC UserInfo
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Aug 13, 2024
1 parent 7bbbfe2 commit c358c25
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ private Uni<TokenVerificationResult> verifyTokenUni(Map<String, Object> requestD
resolvedContext.oidcConfig.token.isSubjectRequired(), nonce));
} catch (Throwable t) {
if (t.getCause() instanceof UnresolvableKeyException) {
LOG.debug("No matching JWK key is found, refreshing and repeating the verification");
LOG.debug("No matching JWK key is found, refreshing and repeating the token verification");
return refreshJwksAndVerifyTokenUni(resolvedContext, token, enforceAudienceVerification,
resolvedContext.oidcConfig.token.isSubjectRequired(), nonce);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcProviderClient.UserInfoResponse;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.credential.TokenCredential;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
Expand All @@ -65,6 +66,7 @@ public class OidcProvider implements Closeable {
AlgorithmConstraints.ConstraintType.PERMIT, ASYMMETRIC_SUPPORTED_ALGORITHMS);
private static final AlgorithmConstraints SYMMETRIC_ALGORITHM_CONSTRAINTS = new AlgorithmConstraints(
AlgorithmConstraints.ConstraintType.PERMIT, SignatureAlgorithm.HS256.getAlgorithm());
private static final String APPLICATION_JWT_CONTENT_TYPE = "application/jwt";
static final String ANY_ISSUER = "any";

private final List<Validator> customValidators;
Expand Down Expand Up @@ -407,7 +409,40 @@ private static final long now() {
}

public Uni<UserInfo> getUserInfo(String accessToken) {
return client.getUserInfo(accessToken);
return client.getUserInfo(accessToken).onItem()
.transformToUni(new Function<UserInfoResponse, Uni<? extends UserInfo>>() {

@Override
public Uni<UserInfo> apply(UserInfoResponse response) {
if (APPLICATION_JWT_CONTENT_TYPE.equals(response.contentType())) {
if (oidcConfig.jwks.resolveEarly) {
try {
LOG.debugf("Verifying the signed UserInfo with the local JWK keys: %s", response.data());
return Uni.createFrom().item(
new UserInfo(
verifyJwtToken(response.data(), true, false, null).localVerificationResult
.encode()));
} catch (Throwable t) {
if (t.getCause() instanceof UnresolvableKeyException) {
LOG.debug(
"No matching JWK key is found, refreshing and repeating the signed UserInfo verification");
return refreshJwksAndVerifyJwtToken(response.data(), true, false, null)
.onItem().transform(v -> new UserInfo(v.localVerificationResult.encode()));
} else {
LOG.debugf("Signed UserInfo verification has failed: %s", t.getMessage());
return Uni.createFrom().failure(t);
}
}
} else {
return getKeyResolverAndVerifyJwtToken(new TokenCredential(response.data(), "userinfo"), true,
false, null, true)
.onItem().transform(v -> new UserInfo(v.localVerificationResult.encode()));
}
} else {
return Uni.createFrom().item(new UserInfo(response.data()));
}
}
});
}

public Uni<AuthorizationCodeTokens> getCodeFlowTokens(String code, String redirectUri, String codeVerifier) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.UserInfo;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
Expand All @@ -36,7 +35,6 @@
public class OidcProviderClient implements Closeable {
private static final Logger LOG = Logger.getLogger(OidcProviderClient.class);

private static final String TENANT_ID_ATTRIBUTE = "oidc-tenant-id";
private static final String AUTHORIZATION_HEADER = String.valueOf(HttpHeaders.AUTHORIZATION);
private static final String CONTENT_TYPE_HEADER = String.valueOf(HttpHeaders.CONTENT_TYPE);
private static final String ACCEPT_HEADER = String.valueOf(HttpHeaders.ACCEPT);
Expand Down Expand Up @@ -93,7 +91,7 @@ public Uni<JsonWebKeySet> getJsonWebKeySet(OidcRequestContextProperties contextP
.transform(resp -> getJsonWebKeySet(resp));
}

public Uni<UserInfo> getUserInfo(String token) {
public Uni<UserInfoResponse> getUserInfo(String token) {
LOG.debugf("Get UserInfo on: %s auth: %s", metadata.getUserInfoUri(), OidcConstants.BEARER_SCHEME + " " + token);
return OidcCommonUtils
.sendRequest(vertx,
Expand Down Expand Up @@ -221,8 +219,8 @@ private AuthorizationCodeTokens getAuthorizationCodeTokens(HttpResponse<Buffer>
return new AuthorizationCodeTokens(idToken, accessToken, refreshToken, tokenExpiresIn);
}

private UserInfo getUserInfo(HttpResponse<Buffer> resp) {
return new UserInfo(getString(metadata.getUserInfoUri(), resp));
private UserInfoResponse getUserInfo(HttpResponse<Buffer> resp) {
return new UserInfoResponse(resp.getHeader(CONTENT_TYPE_HEADER), getString(metadata.getUserInfoUri(), resp));
}

private TokenIntrospection getTokenIntrospection(HttpResponse<Buffer> resp) {
Expand Down Expand Up @@ -290,4 +288,7 @@ public Vertx getVertx() {
public WebClient getWebClient() {
return client;
}

static record UserInfoResponse(String contentType, String data) {
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.application-type=hybri
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.token-path=access_token_refreshed
quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.user-info-path=protocol/openid-connect/userinfo
quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.user-info-path=protocol/openid-connect/signeduserinfo
quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.code-grant.extra-params.extra-param=extra-param-value
quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.code-grant.headers.X-Custom=XCustomHeaderValue
Expand Down Expand Up @@ -233,6 +233,8 @@ quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".min-level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProviderClient".level=TRACE
quarkus.log.file.enable=true
quarkus.log.file.format=%C - %s%n

quarkus.http.auth.permission.logout.paths=/code-flow/logout
quarkus.http.auth.permission.logout.policy=authenticated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,33 @@
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static com.github.tomakehurst.wiremock.client.WireMock.notContaining;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
import static org.awaitility.Awaitility.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.util.Date;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;

import javax.crypto.SecretKey;

import org.awaitility.core.ThrowingRunnable;
import org.hamcrest.Matchers;
import org.htmlunit.SilentCssErrorHandler;
import org.htmlunit.TextPage;
Expand All @@ -33,6 +43,7 @@
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.util.Cookie;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

Expand All @@ -49,6 +60,7 @@
import io.restassured.http.ContentType;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.util.KeyUtils;
import io.vertx.core.json.JsonObject;

Expand Down Expand Up @@ -361,6 +373,43 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception {
.body(Matchers.equalTo("alice:alice:alice-certificate, cache size: 0, TenantConfigResolver: false"));

clearCache();
checkSignedUserInfoRecordInLog();
}

private void checkSignedUserInfoRecordInLog() {
final Path logDirectory = Paths.get(".", "target");
given().await().pollInterval(100, TimeUnit.MILLISECONDS)
.atMost(10, TimeUnit.SECONDS)
.untilAsserted(new ThrowingRunnable() {
@Override
public void run() throws Throwable {
Path accessLogFilePath = logDirectory.resolve("quarkus.log");
boolean fileExists = Files.exists(accessLogFilePath);
if (!fileExists) {
accessLogFilePath = logDirectory.resolve("target/quarkus.log");
fileExists = Files.exists(accessLogFilePath);
}
Assertions.assertTrue(Files.exists(accessLogFilePath),
"quarkus log file " + accessLogFilePath + " is missing");

boolean signedUserInfoLogDetected = false;

try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(accessLogFilePath)),
StandardCharsets.UTF_8))) {
String line = null;
while ((line = reader.readLine()) != null) {
if (line.contains("Verifying the signed UserInfo with the local JWK keys: ey")) {
signedUserInfoLogDetected = true;
break;
}
}
}
assertTrue(signedUserInfoLogDetected,
"Log file must contain a record confirming that signed UserInfo is returned");

}
});
}

@Test
Expand Down Expand Up @@ -564,6 +613,18 @@ private void defineCodeFlowUserInfoCachedInIdTokenStub() {
+ "\"expires_in\": 305"
+ "}")));

wireMockServer.stubFor(
get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/signeduserinfo"))
.withHeader("Authorization", containing("Bearer ey"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/jwt")
.withBody(
Jwt.preferredUserName("alice")
.issuer("https://server.example.com")
.audience("quarkus-web-app")
.jws()
.keyId("1").sign("privateKey.jwk"))));

}

private void defineCodeFlowTokenIntrospectionStub() {
Expand Down

0 comments on commit c358c25

Please sign in to comment.