diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index f6c180a979e647..03acfd1626ede1 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -34,14 +34,22 @@ public class OidcClientConfig extends OidcClientCommonConfig { public Optional> scopes = Optional.empty(); /** - * Refresh token time skew in seconds. - * If this property is enabled then the configured number of seconds is added to the current time + * Refresh token time skew. + * If this property is enabled then the configured duration is converted to seconds and is added to the current time * when checking whether the access token should be refreshed. If the sum is greater than this access token's * expiration time then a refresh is going to happen. */ @ConfigItem public Optional refreshTokenTimeSkew = Optional.empty(); + /** + * Access token expiration period relative to the current time. + * This property is only checked when an access token grant response + * does not include an access token expiration property. + */ + @ConfigItem + public Optional accessTokenExpiresIn = Optional.empty(); + /** * If the access token 'expires_in' property should be checked as an absolute time value * as opposed to a duration relative to the current time. diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java index dc63d9ee40a09d..fab6aacf510ae7 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java @@ -244,7 +244,8 @@ private Tokens emitGrantTokens(OidcRequestContextProperties requestProps, HttpRe JsonObject json = buffer.toJsonObject(); // access token final String accessToken = json.getString(oidcConfig.grant.accessTokenProperty); - final Long accessTokenExpiresAt = getExpiresAtValue(accessToken, json.getValue(oidcConfig.grant.expiresInProperty)); + final Long accessTokenExpiresAt = getAccessTokenExpiresAtValue(accessToken, + json.getValue(oidcConfig.grant.expiresInProperty)); final String refreshToken = json.getString(oidcConfig.grant.refreshTokenProperty); final Long refreshTokenExpiresAt = getExpiresAtValue(refreshToken, @@ -261,6 +262,15 @@ private Tokens emitGrantTokens(OidcRequestContextProperties requestProps, HttpRe } } + private Long getAccessTokenExpiresAtValue(String token, Object expiresInValue) { + Long expiresAt = getExpiresAtValue(token, expiresInValue); + if (expiresAt == null && oidcConfig.accessTokenExpiresIn.isPresent()) { + final long now = System.currentTimeMillis() / 1000; + expiresAt = now + oidcConfig.accessTokenExpiresIn.get().toSeconds(); + } + return expiresAt; + } + private Long getExpiresAtValue(String token, Object expiresInValue) { if (expiresInValue != null) { long tokenExpiresIn = expiresInValue instanceof Number ? ((Number) expiresInValue).longValue() diff --git a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java index e2a978c5a9cfad..a7a05f39fcf4b8 100644 --- a/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java +++ b/extensions/resteasy-reactive/rest/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/security/SecurityContextOverrideHandler.java @@ -16,6 +16,7 @@ import org.jboss.resteasy.reactive.server.model.ServerResourceMethod; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; +import io.quarkus.arc.Arc; import io.quarkus.resteasy.reactive.server.runtime.ResteasyReactiveSecurityContext; import io.quarkus.security.credential.Credential; import io.quarkus.security.identity.CurrentIdentityAssociation; @@ -45,11 +46,11 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti updateIdentity(requestContext, modified); } - private void updateIdentity(ResteasyReactiveRequestContext requestContext, SecurityContext modified) { + private static void updateIdentity(ResteasyReactiveRequestContext requestContext, SecurityContext modified) { requestContext.requireCDIRequestScope(); - if (EagerSecurityContext.instance.identityAssociation.isResolvable()) { + final CurrentIdentityAssociation currentIdentityAssociation = getIdentityAssociation(); + if (currentIdentityAssociation != null) { RoutingContext routingContext = requestContext.unwrap(RoutingContext.class); - CurrentIdentityAssociation currentIdentityAssociation = EagerSecurityContext.instance.identityAssociation.get(); Uni oldIdentity = currentIdentityAssociation.getDeferredIdentity(); currentIdentityAssociation.setIdentity(oldIdentity.map(new Function() { @Override @@ -119,6 +120,15 @@ public Uni checkPermission(Permission permission) { } } + private static CurrentIdentityAssociation getIdentityAssociation() { + if (EagerSecurityContext.instance != null) { + return EagerSecurityContext.instance.identityAssociation.orElse(null); + } + // this should only happen when Quarkus Security extension is not present + // but user implements security themselves, like in their own JAX-RS filter + return Arc.container().instance(CurrentIdentityAssociation.class).orElse(null); + } + public static class Customizer implements HandlerChainCustomizer { @Override public List handlers(Phase phase, ResourceClass resourceClass, diff --git a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java index bdc9234341acbe..76564a6e68857f 100644 --- a/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java +++ b/integration-tests/oidc-client-wiremock/src/main/java/io/quarkus/it/keycloak/FrontendResource.java @@ -37,6 +37,10 @@ public class FrontendResource { @NamedOidcClient("non-standard-response") Tokens tokens; + @Inject + @NamedOidcClient("configured-expires-in") + Tokens tokensConfiguredExpiresIn; + @Inject @NamedOidcClient("non-standard-response-without-header") OidcClient tokensWithoutHeader; @@ -82,6 +86,16 @@ public String echoTokenNonStandardResponse() { } } + @GET + @Path("echoTokenConfiguredExpiresIn") + public String echoTokenConfiguredExpiresIn() { + try { + return tokensConfiguredExpiresIn.getAccessToken() + " " + tokensConfiguredExpiresIn.getAccessTokenExpiresAt(); + } catch (OidcClientException ex) { + throw new WebApplicationException(401); + } + } + @GET @Path("echoTokenNonStandardResponseWithoutHeader") public Uni echoTokenNonStandardResponseWithoutHeader() { diff --git a/integration-tests/oidc-client-wiremock/src/main/resources/application.properties b/integration-tests/oidc-client-wiremock/src/main/resources/application.properties index c50dc0f5d0215d..9285f92bbb684b 100644 --- a/integration-tests/oidc-client-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-client-wiremock/src/main/resources/application.properties @@ -7,6 +7,12 @@ quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice +quarkus.oidc-client.configured-expires-in.token-path=${keycloak.url}/tokens-without-expires-in +quarkus.oidc-client.configured-expires-in.client-id=quarkus-app +quarkus.oidc-client.configured-expires-in.credentials.client-secret.value=secret +quarkus.oidc-client.configured-expires-in.credentials.client-secret.method=post +quarkus.oidc-client.configured-expires-in.access-token-expires-in=5S + quarkus.oidc-client.jwtbearer.auth-server-url=${keycloak.url} quarkus.oidc-client.jwtbearer.discovery-enabled=false quarkus.oidc-client.jwtbearer.token-path=/tokens-jwtbearer diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index ec1fe35f18fa07..ea3ba7842ea0c2 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -79,6 +79,14 @@ public Map start() { .withBody( "{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_2\", \"refresh_expires_in\":1}"))); + server.stubFor(WireMock.post("/tokens-without-expires-in") + .withRequestBody(matching("grant_type=client_credentials&client_id=quarkus-app&client_secret=secret")) + .willReturn(WireMock + .aResponse() + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody( + "{\"access_token\":\"access_token_without_expires_in\"}"))); + server.stubFor(WireMock.post("/refresh-token-only") .withRequestBody( matching("grant_type=refresh_token&refresh_token=shared_refresh_token&extra_param=extra_param_value")) diff --git a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java index b21fa0c95f83ec..f02e864ce4e2ba 100644 --- a/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java +++ b/integration-tests/oidc-client-wiremock/src/test/java/io/quarkus/it/keycloak/OidcClientTest.java @@ -5,6 +5,7 @@ import static org.awaitility.Awaitility.given; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -28,6 +29,7 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; +import io.restassured.response.Response; @QuarkusTest @QuarkusTestResource(KeycloakRealmResourceManager.class) @@ -44,6 +46,21 @@ public void testEchoTokensJwtBearerAuthentication() { .body(equalTo("access_token_jwt_bearer")); } + @Test + public void testGetAccessTokenWithConfiguredExpiresIn() { + Response r = RestAssured.when().get("/frontend/echoTokenConfiguredExpiresIn"); + assertEquals(200, r.statusCode()); + String[] data = r.body().asString().split(" "); + assertEquals(2, data.length); + assertEquals("access_token_without_expires_in", data[0]); + + long now = System.currentTimeMillis() / 1000; + long expectedExpiresAt = now + 5; + long accessTokenExpiresAt = Long.valueOf(data[1]); + assertTrue(accessTokenExpiresAt >= expectedExpiresAt + && accessTokenExpiresAt <= expectedExpiresAt + 2); + } + @Test public void testEchoTokensJwtBearerGrant() { RestAssured.when().get("/frontend/echoTokenJwtBearerGrant")