Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check the expiry date for inactive OIDC tokens #31811

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ public Uni<SecurityIdentity> apply(UserInfo userInfo, Throwable t) {
@Override
public Uni<SecurityIdentity> apply(TokenVerificationResult codeAccessTokenResult, Throwable t) {
if (t != null) {
return Uni.createFrom().failure(new AuthenticationFailedException(t));
return Uni.createFrom().failure(t instanceof AuthenticationFailedException ? t
: new AuthenticationFailedException(t));
}
if (codeAccessTokenResult != null) {
if (tokenAutoRefreshPrepared(codeAccessTokenResult, vertxContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,10 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl
}
if (!Boolean.TRUE.equals(introspectionResult.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) {
LOG.debugf("Token issued to client %s is not active", oidcConfig.clientId.get());
verifyTokenExpiry(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP));
throw new AuthenticationFailedException();
}
if (isTokenExpired(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP))) {
String error = String.format("Token issued to client %s has expired",
oidcConfig.clientId.get());
LOG.debugf(error);
throw new AuthenticationFailedException(
new InvalidJwtException(error,
List.of(new ErrorCodeValidator.Error(ErrorCodes.EXPIRED, error)), null));
}
verifyTokenExpiry(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP));
try {
verifyTokenAge(introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_IAT));
} catch (InvalidJwtException ex) {
Expand All @@ -246,6 +240,17 @@ public TokenIntrospection apply(TokenIntrospection introspectionResult, Throwabl
return introspectionResult;
}

private void verifyTokenExpiry(Long exp) {
if (isTokenExpired(exp)) {
String error = String.format("Token issued to client %s has expired",
oidcConfig.clientId.get());
LOG.debugf(error);
throw new AuthenticationFailedException(
new InvalidJwtException(error,
List.of(new ErrorCodeValidator.Error(ErrorCodes.EXPIRED, error)), null));
}
}

});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.it.keycloak;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

@Path("/code-flow-token-introspection")
@Authenticated
public class CodeFlowTokenIntrospectionResource {

@Inject
SecurityIdentity identity;

@GET
public String access() {
return identity.getPrincipal().getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRe
|| 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-token-introspection")
|| routingContext.normalizedPath().endsWith("code-flow-user-info-github-cached-in-idtoken"))) {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
UserInfo userInfo = identity.getAttribute("userinfo");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public String resolve(RoutingContext context) {
if (path.endsWith("code-flow-user-info-github-cached-in-idtoken")) {
return "code-flow-user-info-github-cached-in-idtoken";
}
if (path.endsWith("code-flow-token-introspection")) {
return "code-flow-token-introspection";
}
if (path.endsWith("bearer")) {
return "bearer";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.client-id=quarkus-web-
quarkus.oidc.code-flow-user-info-github-cached-in-idtoken.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow


quarkus.oidc.code-flow-token-introspection.provider=github
quarkus.oidc.code-flow-token-introspection.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.code-flow-token-introspection.authorization-path=/
quarkus.oidc.code-flow-token-introspection.user-info-path=protocol/openid-connect/userinfo
quarkus.oidc.code-flow-token-introspection.introspection-path=protocol/openid-connect/token/introspect
quarkus.oidc.code-flow-token-introspection.token.refresh-expired=true
quarkus.oidc.code-flow-token-introspection.token.refresh-token-time-skew=298
quarkus.oidc.code-flow-token-introspection.authentication.verify-access-token=true
quarkus.oidc.code-flow-token-introspection.client-id=quarkus-web-app
quarkus.oidc.code-flow-token-introspection.credentials.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
quarkus.oidc.code-flow-token-introspection.code-grant.headers.X-Custom=XTokenIntrospection

quarkus.oidc.token-cache.max-size=1

quarkus.oidc.bearer.auth-server-url=${keycloak.url}/realms/quarkus/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,30 @@ public void testCodeFlowUserInfo() throws Exception {
doTestCodeFlowUserInfoCashedInIdToken();
}

@Test
public void testCodeFlowTokenIntrospection() throws Exception {
defineCodeFlowTokenIntrospectionStub();
try (final WebClient webClient = createWebClient()) {
webClient.getOptions().setRedirectEnabled(true);
HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-token-introspection");

HtmlForm form = page.getFormByName("form");
form.getInputByName("username").type("alice");
form.getInputByName("password").type("alice");

page = form.getInputByValue("login").click();

assertEquals("alice", page.getBody().asNormalizedText());

// refresh
Thread.sleep(3000);
page = webClient.getPage("http://localhost:8081/code-flow-token-introspection");
assertEquals("admin", page.getBody().asNormalizedText());

webClient.getCookieManager().clearCookies();
}
}

private void doTestCodeFlowUserInfo(String tenantId, long internalIdTokenLifetime) throws IOException {
try (final WebClient webClient = createWebClient()) {
webClient.getOptions().setRedirectEnabled(true);
Expand Down Expand Up @@ -297,6 +321,28 @@ private void defineCodeFlowAuthorizationOauth2TokenStub() {

}

private void defineCodeFlowTokenIntrospectionStub() {
wireMockServer
.stubFor(WireMock.post("/auth/realms/quarkus/access_token")
.withHeader("X-Custom", matching("XTokenIntrospection"))
.withRequestBody(containing("authorization_code"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"access_token\": \"alice\","
+ " \"refresh_token\": \"refresh5678\""
+ "}")));

wireMockServer
.stubFor(WireMock.post("/auth/realms/quarkus/access_token")
.withRequestBody(containing("refresh_token=refresh5678"))
.willReturn(WireMock.aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"access_token\": \"admin\""
+ "}")));
}

private void defineCodeFlowLogoutStub() {
wireMockServer.stubFor(
get(urlPathMatching("/auth/realms/quarkus/protocol/openid-connect/end-session"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,9 @@ public Map<String, String> start() {
" ]\n" +
"}")));

server.stubFor(
get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"preferred_username\": \"alice\""
+ "}")));
defineUserInfoStubForOpaqueToken("alice");
defineUserInfoStubForOpaqueToken("admin");
defineUserInfoStubForJwt();

// define the mock for the introspect endpoint

Expand Down Expand Up @@ -221,7 +217,30 @@ public Map<String, String> start() {
return conf;
}

private void defineUserInfoStubForOpaqueToken(String user) {
server.stubFor(
get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo"))
.withHeader("Authorization", matching("Bearer " + user))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"preferred_username\": \"" + user + "\""
+ "}")));
}

private void defineUserInfoStubForJwt() {
server.stubFor(
get(urlEqualTo("/auth/realms/quarkus/protocol/openid-connect/userinfo"))
.withHeader("Authorization", containing("Bearer ey"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\n" +
" \"preferred_username\": \"alice\""
+ "}")));
}

private void defineValidIntrospectionMockTokenStubForUserWithRoles(String user, Set<String> roles) {
long exp = now() + 300;
server.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect")
.withRequestBody(matching("token=" + user + "&token_type_hint=access_token"))
.willReturn(WireMock
Expand All @@ -230,7 +249,12 @@ private void defineValidIntrospectionMockTokenStubForUserWithRoles(String user,
.withBody(
"{\"active\":true,\"scope\":\"" + roles.stream().collect(joining(" ")) + "\",\"username\":\""
+ user
+ "\",\"iat\":1,\"exp\":999999999999,\"expires_in\":999999999999,\"client_id\":\"my_client_id\"}")));
+ "\",\"iat\":1,\"exp\":" + exp + ",\"expires_in\":" + exp
+ ",\"client_id\":\"my_client_id\"}")));
}

private static final long now() {
return System.currentTimeMillis();
}

private void defineInvalidIntrospectionMockTokenStubForUserWithRoles(String user, Set<String> roles) {
Expand Down