Skip to content

Commit

Permalink
Merge pull request #31811 from sberyozkin/oidc_check_exp_for_inactive…
Browse files Browse the repository at this point in the history
…_token

Check the expiry date for inactive OIDC tokens
  • Loading branch information
gastaldi authored Mar 22, 2023
2 parents ce446c7 + 402a9ba commit e89ccc2
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 17 deletions.
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

0 comments on commit e89ccc2

Please sign in to comment.