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

Add a test verifying only JWT introspection can be used and update the docs #17953

Merged
merged 1 commit into from
Jun 18, 2021
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 @@ -417,6 +417,7 @@ The current tenant's discovered link:https://openid.net/specs/openid-connect-dis

The default tenant's `OidcConfigurationMetadata` is injected if the endpoint is public.

[[token-claims-roles]]
== Token Claims And SecurityIdentity Roles

The way the roles are mapped to the SecurityIdentity roles from the verified tokens is identical to how it is done for the link:security-openid-connect#token-claims-and-securityidentity-roles[bearer tokens] with the only difference being is that https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID Token] is used as a source of the roles by default.
Expand All @@ -429,6 +430,13 @@ If UserInfo is the source of the roles then set `quarkus.oidc.authentication.use

Additionally a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented link:security#security-identity-customization[here].

[[token-verification-introspection]]
== Token Verification And Introspection

Please see link:security-openid-connect#token-verification-introspection for details about how the tokens are verified and introspected.

Note that in case of `web-app` applications only `IdToken` is verified by default since the access token is not used by default to access the current Quarkus `web-app` endpoint and instead meant to be propagated to the services expecting this access token, for example, to the OpenId Connect Provider's UserInfo endpoint, etc. However if you expect the access token to contain the roles required to access the current Quarkus endpoint (`quarkus.oidc.roles.source=accesstoken`) then it will also be verified.

[[session-management]]
== Session Management

Expand Down
41 changes: 29 additions & 12 deletions docs/src/main/asciidoc/security-openid-connect.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -336,18 +336,6 @@ Set `quarkus.oidc.user-info-required=true` if a UserInfo JSON object from the OI
A request will be sent to the OpenId Provider UserInfo endpoint and an `io.quarkus.oidc.UserInfo` (a simple `javax.json.JsonObject` wrapper) object will be created.
`io.quarkus.oidc.UserInfo` can be either injected or accessed as a SecurityIdentity `userinfo` attribute.

[[token-introspection]]
== Token Introspection

An opaque token has to be introspected by sending it to the OpenId Provider token introspection endpoint.

If the opaque token is active then a token introspection `username` and `scope` properties will be used to build a `Securityidentity`. Additionally, an `io.quarkus.oidc.TokenIntrospection` (a simple `javax.json.JsonObject` wrapper) object will be created and can be either injected or accessed as a SecurityIdentity `introspection` attribute.

Signed JWT tokens can also be introspected when no local matching `JsonWebKey` is available.

If you only work with JWT tokens then it is recommended to disable the opaque token introspection with `quarkus.oidc.token.allow-opaque-token-introspection=false`.
Additionally, disabling the introspection of signed JWT tokens is also advised with `quarkus.oidc.token.allow-jwt-introspection=false` if you expect that a local `JsonWebKey` will always be available since a 7`JsonWebKeySet` containing the public verification keys is periodically refreshed when the token has no matching `JsonWebKey`.

[[config-metadata]]
== Configuration Metadata

Expand All @@ -371,6 +359,35 @@ If UserInfo is the source of the roles then set `quarkus.oidc.authentication.use

Additionally a custom `SecurityIdentityAugmentor` can also be used to add the roles as documented link:security#security-identity-customization[here].

[[token-verification-introspection]]
== Token Verification And Introspection

If the token is a JWT token then, by default, it will be verified with a `JsonWebKey` (JWK) key from a local `JsonWebKeySet` retrieved from the OpenId Connect Provider's JWK endpoint. The token's key identifier `kid` header value will be used to find the matching JWK key.
If no matching `JWK` is available locally then `JsonWebKeySet` will be refreshed by fetching the current key set from the JWK endpoint. The `JsonWebKeySet` refresh can be repeated again only after the `quarkus.oidc.token.forced-jwk-refresh-interval` (default is 10 minutes) expires.
If no matching `JWK` is available after the refresh then the JWT token will be sent to the OpenId Connect Provider's token introspection endpoint.

If the token is opaque (it can be a binary token or an encrypted JWT token) then it will always be sent to the OpenId Connect Provider's token introspection endpoint.

If you work with JWT tokens only and expect that a matching `JsonWebKey` will always be available (possibly after a key set refresh) then you should disable the token introspection:

[source, properties]
----
quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false
----

However, there could be cases where JWT tokens must be verified via the introspection only. It can be forced by configuring an introspection endpoint address only, for example, in case of Keycloak you can do it like this:

[source, properties]
----
quarkus.oidc.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Token Introspection endoint: http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
----

Note that `io.quarkus.oidc.TokenIntrospection` (a simple `javax.json.JsonObject` wrapper) object will be created and can be either injected or accessed as a SecurityIdentity `introspection` attribute if either JWT or opaque token has been successfully introspected.

[[single-page-applications]]
== Single Page Applications

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public TokenVerificationResult apply(TokenIntrospection introspectionResult, Thr
throw new AuthenticationFailedException(t);
}
if (!Boolean.TRUE.equals(introspectionResult.getBoolean(OidcConstants.INTROSPECTION_TOKEN_ACTIVE))) {
LOG.debugf("Token issued to client %s is not active: %s", oidcConfig.clientId.get());
throw new AuthenticationFailedException();
}
Long exp = introspectionResult.getLong(OidcConstants.INTROSPECTION_TOKEN_EXP);
Expand All @@ -161,6 +162,7 @@ public TokenVerificationResult apply(TokenIntrospection introspectionResult, Thr
? client.getOidcConfig().token.lifespanGrace.getAsInt()
: 0;
if (System.currentTimeMillis() / 1000 > exp + lifespanGrace) {
LOG.debugf("Token issued to client %s has expired %s", oidcConfig.clientId.get());
throw new AuthenticationFailedException();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ public OidcTenantConfig resolve(RoutingContext context) {
config.token.setAllowJwtIntrospection(false);
config.setClientId("client");
return config;
} else if ("tenant-oidc-introspection-only".equals(tenantId)) {
OidcTenantConfig config = new OidcTenantConfig();
config.setTenantId("tenant-oidc-introspection-only");
String uri = context.request().absoluteURI();
String authServerUri = uri.replace("/tenant/tenant-oidc-introspection-only/api/user", "/oidc");
config.setAuthServerUrl(authServerUri);
config.setDiscoveryEnabled(false);
config.setIntrospectionPath("introspect");
config.setClientId("client");
return config;
} else if ("tenant-oidc-no-opaque-token".equals(tenantId)) {
OidcTenantConfig config = new OidcTenantConfig();
config.setTenantId("tenant-oidc-no-opaque-token");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,23 @@ public void testJwtTokenIntrospectionDisallowed() {
RestAssured.when().post("/oidc/disable-rotate").then().body(equalTo("false"));
}

@Test
public void testJwtTokenIntrospectionOnly() {
RestAssured.when().post("/oidc/jwk-endpoint-call-count").then().body(equalTo("0"));
RestAssured.when().post("/oidc/introspection-endpoint-call-count").then().body(equalTo("0"));
RestAssured.when().post("/oidc/enable-introspection").then().body(equalTo("true"));

// JWK is available now in Quarkus OIDC, confirm that no timeout is needed
RestAssured.given().auth().oauth2(getAccessTokenFromSimpleOidc("2"))
.when().get("/tenant/tenant-oidc-introspection-only/api/user")
.then()
.statusCode(200)
.body(equalTo("tenant-oidc-introspection-only:alice"));

RestAssured.when().get("/oidc/jwk-endpoint-call-count").then().body(equalTo("0"));
RestAssured.when().get("/oidc/introspection-endpoint-call-count").then().body(equalTo("1"));
}

@Test
public void testSimpleOidcNoDiscovery() {
RestAssured.when().post("/oidc/jwk-endpoint-call-count").then().body(equalTo("0"));
Expand Down