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

Allow configuring OIDC state cookie age #40316

Merged
merged 1 commit into from
Apr 26, 2024
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
42 changes: 26 additions & 16 deletions docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
However, if the requirement is for the current application to log the user out of a specific application only, you can override the global end-session URL, by setting the `quarkus.oidc.end-session-path=logout` parameter.

[[oidc-provider-client-authentication]]
==== OIDC provider client authentication
=== OIDC provider client authentication

OIDC providers typically require applications to be identified and authenticated when they interact with the OIDC endpoints.
Quarkus OIDC, specifically the `quarkus.oidc.runtime.OidcProviderClient` class, authenticates to the OIDC provider when the authorization code must be exchanged for the ID, access, and refresh tokens, or when the ID and access tokens must be refreshed or introspected.
Expand Down Expand Up @@ -203,7 +203,7 @@

Using `client_secret_jwt` or `private_key_jwt` authentication methods ensures that a client secret does not get sent to the OIDC provider, therefore avoiding the risk of a secret being intercepted by a 'man-in-the-middle' attack.

===== Additional JWT authentication options
==== Additional JWT authentication options

Check warning on line 206 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Additional JWT authentication options'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Additional JWT authentication options'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 206, "column": 6}}}, "severity": "INFO"}

If `client_secret_jwt`, `private_key_jwt`, or an Apple `post_jwt` authentication methods are used, then you can customize the JWT signature algorithm, key identifier, audience, subject and issuer.
For example:
Expand Down Expand Up @@ -234,7 +234,7 @@
quarkus.oidc.credentials.jwt.issuer=custom-issuer
----

===== Apple POST JWT
==== Apple POST JWT

Check warning on line 237 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Apple POST JWT'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Apple POST JWT'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 237, "column": 6}}}, "severity": "INFO"}

The Apple OIDC provider uses a `client_secret_post` method whereby a secret is a JWT produced with a `private_key_jwt` authentication method, but with the Apple account-specific issuer and subject claims.

Expand All @@ -254,9 +254,9 @@
quarkus.oidc.credentials.jwt.issuer=${apple.issuer}
----

===== mutual TLS (mTLS)
==== mutual TLS (mTLS)

Some OIDC providers might require that a client is authenticated as part of the mutual TLS authentication process.

Check warning on line 259 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 259, "column": 66}}}, "severity": "INFO"}

The following example shows how you can configure `quarkus-oidc` to support `mTLS`:

Expand All @@ -279,9 +279,9 @@
#quarkus.oidc.tls.trust-store-alias=certAlias
----

===== POST query
==== POST query

Check warning on line 282 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'POST query'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'POST query'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 282, "column": 6}}}, "severity": "INFO"}

Some providers, such as the xref:security-openid-connect-providers#strava[Strava OAuth2 provider], require client credentials be posted as HTTP POST query parameters:

Check warning on line 284 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 284, "column": 22}}}, "severity": "INFO"}

[source,properties]
----
Expand All @@ -305,7 +305,7 @@
----

[[oidc-request-filters]]
==== OIDC request filters
=== OIDC request filters

You can filter OIDC requests made by Quarkus to the OIDC provider by registering one or more `OidcRequestFilter` implementations, which can update or add new request headers and can also log requests.

Expand Down Expand Up @@ -369,9 +369,9 @@
----
<1> Restrict this filter to requests targeting the OIDC discovery endpoint only.

==== Redirecting to and from the OIDC provider
=== Redirecting to and from the OIDC provider

When a user is redirected to the OIDC provider to authenticate, the redirect URL includes a `redirect_uri` query parameter, which indicates to the provider where the user has to be redirected to when the authentication is complete.

Check warning on line 374 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 374, "column": 1}}}, "severity": "INFO"}
In our case, this is the Quarkus application.

Quarkus sets this parameter to the current application request URL by default.
Expand Down Expand Up @@ -572,11 +572,11 @@
It applies to ID tokens and also to access tokens in a JWT format, if the `web-app` application has requested the access token verification.

[[jose4j-validator]]
=== Jose4j Validator
==== Jose4j Validator

Check warning on line 575 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Jose4j Validator'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Jose4j Validator'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 575, "column": 6}}}, "severity": "INFO"}

You can register a custom [Jose4j Validator] to customize the JWT claim verification process. See xref:security-oidc-bearer-token-authentication.adoc#jose4j-validator[Jose4j] section for more information.

==== Further security with Proof Key for Code Exchange (PKCE)
=== Proof Key for Code Exchange (PKCE)

Check warning on line 579 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Proof Key for Code Exchange (PKCE)'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Proof Key for Code Exchange (PKCE)'.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 579, "column": 5}}}, "severity": "INFO"}

link:https://datatracker.ietf.org/doc/html/rfc7636[Proof Key for Code Exchange] (PKCE) minimizes the risk of authorization code interception.

Expand All @@ -598,7 +598,6 @@
The `code_verifier` is decrypted when the user is redirected back to Quarkus and sent to the token endpoint alongside the `code`, client secret, and other parameters to complete the code exchange.
The provider will fail the code exchange if a `SHA256` digest of the `code_verifier` does not match the `code_challenge` that was provided during the authentication request.


=== Handling and controlling the lifetime of authentication

Another important requirement for authentication is to ensure that the data the session is based on is up-to-date without requiring the user to authenticate for every single request.
Expand Down Expand Up @@ -632,6 +631,17 @@
* \https://whatever.wherever.company.net/
* \https://another.address.company.net/

[[state-cookies]]
==== State cookies

State cookies are used to support authorization code flow completion.
When an authorization code flow is started, Quarkus creates a state cookie and a matching `state` query parameter, before redirecting the user to the OIDC provider.

Check warning on line 638 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 638, "column": 1}}}, "severity": "INFO"}
When the user is redirected back to Quarkus to complete the authorization code flow, Quarkus expects that the request URI must contain the `state` query parameter and it must match the current state cookie value.

The default state cookie age is 5 mins and you can change it with a `quarkus.oidc.authenticaion.state-cookie-age` Duration property.

Check warning on line 641 in docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'mins'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'mins'?", "location": {"path": "docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc", "range": {"start": {"line": 641, "column": 35}}}, "severity": "WARNING"}

Quarkus creates a unique state cookie name every time a new authorization code flow is started to support multi-tab authentication. Many concurrent authentication requests on behalf of the same user may cause a lot of state cookies be created.
If you do not want to allow your users use multiple browser tabs to authenticate then it is recommended to disable it with `quarkus.oidc.authenticaion.allow-multiple-code-flows=false`. It also ensures that the same state cookie name is created for every new user authentication.

[[token-state-manager]]
==== Session cookie and default TokenStateManager
Expand Down Expand Up @@ -862,15 +872,15 @@
For more information, refer to the xref:hibernate-orm.adoc[Hibernate ORM] guide.
<2> You can choose a column length depending on the length of your tokens.

==== Logout and expiration
=== Logout and expiration

There are two main ways for the authentication information to expire: the tokens expired and were not renewed or an explicit logout operation was triggered.

Let's start with explicit logout operations.


[[user-initiated-logout]]
===== User-initiated logout
==== User-initiated logout

Users can request a logout by sending a request to the Quarkus endpoint logout path set with a `quarkus.oidc.logout.path` property.
For example, if the endpoint address is `https://application.com/webapp` and the `quarkus.oidc.logout.path` is set to "/logout", then the logout request must be sent to `https://application.com/webapp/logout`.
Expand Down Expand Up @@ -946,7 +956,7 @@
====

[[back-channel-logout]]
===== Back-channel logout
==== Back-channel logout

The OIDC provider can force the logout of all applications by using the authentication data.
This is known as back-channel logout.
Expand All @@ -973,7 +983,7 @@
For example, set `quarkus.oidc.token.age=10S` to ensure that no more than 10 seconds elapse since the logout token's `iat` (issued at) time.

[[front-channel-logout]]
===== Front-channel logout
==== Front-channel logout

You can use link:https://openid.net/specs/openid-connect-frontchannel-1_0.html[Front-channel logout] to log out the current user directly from the user agent, for example, its browser.
It is similar to <<back-channel-logout,Back-channel logout>> but the logout steps are executed by the user agent, such as the browser, and not in the background by the OIDC provider.
Expand All @@ -994,7 +1004,7 @@
This path will be compared to the current request's path, and the user will be logged out if these paths match.

[[local-logout]]
===== Local logout
==== Local logout

<<user-initiated-logout,User-initiated logout>> will log the user out of the OIDC provider.
If it is used as single sign-on, it might not be what you require.
Expand Down Expand Up @@ -1027,7 +1037,7 @@
----

[[oidc-session]]
====== Using `OidcSession` for local logout
==== Using `OidcSession` for local logout

`io.quarkus.oidc.OidcSession` is a wrapper around the current `IdToken`, which can help to perform a <<local-logout,Local logout>>, retrieve the current session's tenant identifier, and check when the session will expire.
More useful methods will be added to it over time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,16 @@ public enum ResponseMode {
@ConfigItem(defaultValue = "5M")
public Duration sessionAgeExtension = Duration.ofMinutes(5);

/**
* State cookie age in minutes.
* State cookie is created every time a new authorization code flow redirect starts
* and removed when this flow is completed.
* State cookie name is unique by default, see {@link #allowMultipleCodeFlows}.
* Keep its age to the reasonable minimum value such as 5 minutes or less.
*/
@ConfigItem(defaultValue = "5M")
public Duration stateCookieAge = Duration.ofMinutes(5);

/**
* If this property is set to `true`, a normal 302 redirect response is returned
* if the request was initiated by a JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be
Expand Down Expand Up @@ -1441,6 +1451,14 @@ public Optional<String> getScopeSeparator() {
public void setScopeSeparator(String scopeSeparator) {
this.scopeSeparator = Optional.of(scopeSeparator);
}

public Duration getStateCookieAge() {
return stateCookieAge;
}

public void setStateCookieAge(Duration stateCookieAge) {
this.stateCookieAge = stateCookieAge;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,8 @@ private String generateCodeFlowState(RoutingContext context, TenantConfigContext
}
String stateCookieNameSuffix = configContext.oidcConfig.authentication.allowMultipleCodeFlows ? "_" + uuid : "";
createCookie(context, configContext.oidcConfig,
getStateCookieName(configContext.oidcConfig) + stateCookieNameSuffix, cookieValue, 60 * 30);
getStateCookieName(configContext.oidcConfig) + stateCookieNameSuffix, cookieValue,
configContext.oidcConfig.authentication.stateCookieAge.toSeconds());
return uuid;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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.urlPathMatching;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
Expand Down Expand Up @@ -277,10 +276,19 @@ public void testCodeFlowUserInfoCachedInIdToken() throws Exception {
form.getInputByName("username").type("alice");
form.getInputByName("password").type("alice");

Cookie stateCookie = getStateCookie(webClient, "code-flow-user-info-github-cached-in-idtoken");
Date stateCookieDate = stateCookie.getExpires();
final long nowInSecs = System.currentTimeMillis() / 1000;
final long sessionCookieLifespan = stateCookieDate.toInstant().getEpochSecond() - nowInSecs;
// 5 mins is default
assertTrue(sessionCookieLifespan >= 299 && sessionCookieLifespan <= 304);

TextPage textPage = form.getInputByValue("login").click();

assertEquals("alice:alice:alice, cache size: 0, TenantConfigResolver: false", textPage.getContent());

assertNull(getStateCookie(webClient, "code-flow-user-info-github-cached-in-idtoken"));

JsonObject idTokenClaims = decryptIdToken(webClient, "code-flow-user-info-github-cached-in-idtoken");
assertNotNull(idTokenClaims.getJsonObject(OidcUtils.USER_INFO_ATTRIBUTE));

Expand Down Expand Up @@ -527,4 +535,10 @@ private void defineCodeFlowLogoutStub() {
private Cookie getSessionCookie(WebClient webClient, String tenantId) {
return webClient.getCookieManager().getCookie("q_session" + (tenantId == null ? "" : "_" + tenantId));
}

private Cookie getStateCookie(WebClient webClient, String tenantId) {
return webClient.getCookieManager().getCookies().stream()
.filter(c -> c.getName().startsWith("q_auth" + (tenantId == null ? "" : "_" + tenantId))).findFirst()
.orElse(null);
}
}
Loading