Skip to content

Commit

Permalink
Merge pull request #40316 from sberyozkin/oidc_state_cookie_age
Browse files Browse the repository at this point in the history
Allow configuring OIDC state cookie age
  • Loading branch information
sberyozkin authored Apr 26, 2024
2 parents ffcbf29 + 24d346f commit 22aedce
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 18 deletions.
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 @@ This URL will log the user out of all the applications into which the user is cu
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 @@ quarkus.oidc.credentials.jwt.key-id=mykeyAlias

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

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.subject=custom-subject
quarkus.oidc.credentials.jwt.issuer=custom-issuer
----

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

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,7 +254,7 @@ quarkus.oidc.credentials.jwt.subject=${apple.subject}
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.

Expand All @@ -279,7 +279,7 @@ quarkus.oidc.tls.trust-store-password=${trust-store-password}
#quarkus.oidc.tls.trust-store-alias=certAlias
----

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

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

Expand All @@ -305,7 +305,7 @@ quarkus.oidc.introspection-credentials.secret=introspection-user-secret
----

[[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,7 +369,7 @@ public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter {
----
<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.
In our case, this is the Quarkus application.
Expand Down Expand Up @@ -572,11 +572,11 @@ For information about the claim verification, including the `iss` (issuer) claim
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

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)

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 secret key is required to encrypt a randomly generated PKCE `code_verifier`
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 @@ For example, if you have Quarkus services deployed on the following two domains,
* \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.
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.

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 @@ public class OidcDbTokenStateManagerEntity {
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 @@ quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
====

[[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 @@ You will also need to configure a token age property for the logout token verifi
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 @@ quarkus.oidc.logout.frontchannel.path=/front-channel-logout
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 @@ public class ServiceResource {
----

[[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);
}
}

0 comments on commit 22aedce

Please sign in to comment.