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

Implement session logout in OAuth2Authenticator #18753

Merged
merged 5 commits into from
Oct 11, 2023

Conversation

Praveen2112
Copy link
Member

@Praveen2112 Praveen2112 commented Aug 21, 2023

Description

When trying to logout from Trino WebUI, allows the user to be redirected to a specific end_session_endpoint thereby allowing user to logout from the IdP/OP.

Specification - https://openid.net/specs/openid-connect-rpinitiated-1_0.html

Will be sharing a video link on how it worked before and after this change.

Fixes #13060

Additional context and related issues

Release notes

( ) This is not user-visible or is docs only, and no release notes are required.
(x) Release notes are required. Please propose a release note for me.
( ) Release notes are required, with the following suggested text:

# Section
* Implement session logout when logging out from Trino WebUI . ({issue}`13060`)

@cla-bot cla-bot bot added the cla-signed label Aug 21, 2023
@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from db55ab9 to 591f8f8 Compare August 22, 2023 06:01
@@ -34,6 +34,8 @@ Response getOAuth2Response(String code, URI callbackUri, Optional<String> nonce)
Response refreshTokens(String refreshToken)
throws ChallengeFailedException;

Optional<URI> getLogoutEndPoint(Optional<String> idToken, URI callbackUrl);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: endpoint is one word so casing seems off

@@ -114,6 +115,7 @@ private OAuth2ServerConfig readConfiguration(String body)
else {
userinfoEndpoint = Optional.empty();
}
Optional<String> endSessionEndpoint = getOptionalField("end_session_endpoint", Optional.ofNullable(metadata.getEndSessionEndpointURI()).map(URI::toString), END_SESSION_URL, Optional.empty());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's required according to the spec:

end_session_endpoint
REQUIRED. URL at the OP to which an RP can perform a redirect to request that the End-User be logged out at the OP. 

.build();
NonceCookie.delete());
if (oauth2Response.getIdToken().isPresent()) {
builder.cookie(OAuthIdTokenCookie.create(oauth2Response.getIdToken().get(), oauth2Response.getExpiration()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't expiration be calculated as for OAuthWebUiCookie above?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But in case of OAuthWebUiCookie we do wrap the token as JWT and pass the expiration time for that, which is independent of the ID token. So shouldn't we use them ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. How this should work for refresh tokens in that case? I think we will get expiration for the access token, let say, 1h but since we will have refresh token we will be able to prolong the session for much longer than that. Using the same expiration for ID token cookie lifetime will mean that the this cookie will expire after 1h and we won't be able to perform logout correctly after that.

@@ -1367,6 +1375,29 @@ private static Principal authenticate2(String user, String password)
throw new AccessDeniedException("Invalid credentials2");
}

private static HttpCookie getWebUiOAuthCookie(CookieManager cookieManager)
{
return cookieManager.getCookieStore().getCookies().stream()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: duplicated fragment, you could extract a method to get the cookie

@@ -836,7 +842,31 @@ public void testCustomPrincipalField()
}
}

private void assertAuth2Authentication(TestingTrinoServer server, String accessToken, boolean refreshTokensEnabled)
@Test
public void testOAuth2AuthenticatorWithoutEndsessionEndpoint()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if http-server.authentication.oauth2.scopes does not contain openid scope but IdP returned end_session_endpoint and the other way around?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, in case of openid scope then the end_session_endopoint is mandatory right ? But if the scope if non opeid then we could optionally provide the end-point which if configured might be redirected. Am I missing something here ?

@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from 591f8f8 to bb3cc23 Compare August 22, 2023 12:10
@Praveen2112
Copy link
Member Author

@lukasz-walkiewicz Thanks for the review. Have addressed them

@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch 3 times, most recently from a60c7f4 to 53d064a Compare September 14, 2023 10:08
@Praveen2112 Praveen2112 marked this pull request as ready for review September 14, 2023 10:35
.path("logout.html")
.build();

return Response.seeOther(auth2Client.getLogoutEndpoint(idToken, callBackUri).orElse(callBackUri))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to call sso logout ourselves behind the scenes instead of depending on user browser's doing that?
Otherwise we could end up in situation where the user doesn't follow that redirect for any reason and the value is still valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec here mentions that

An RP requests that the OP log out the End-User by redirecting the End-User's User Agent to the OP's Logout Endpoint. This URL is normally obtained via the end_session_endpoint element of the OP's Discovery response or may be learned via other mechanisms.

So I guess we should redirect on the User browser instead of handling it. Additionally each IdP can have its own way to redirecting the user right ?

Copy link
Member

@lukasz-walkiewicz lukasz-walkiewicz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, looks good but I think we lack a bit of coverage, especially wrt support for refresh tokens.

.build();
NonceCookie.delete());
if (oauth2Response.getIdToken().isPresent()) {
builder.cookie(OAuthIdTokenCookie.create(oauth2Response.getIdToken().get(), oauth2Response.getExpiration()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. How this should work for refresh tokens in that case? I think we will get expiration for the access token, let say, 1h but since we will have refresh token we will be able to prolong the session for much longer than that. Using the same expiration for ID token cookie lifetime will mean that the this cookie will expire after 1h and we won't be able to perform logout correctly after that.

@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch 6 times, most recently from 7a647ed to 9fcc9d0 Compare October 6, 2023 17:01
@Praveen2112
Copy link
Member Author

@lukasz-walkiewicz Thanks a lot for the review. AC

@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from 9fcc9d0 to 0ac0b57 Compare October 9, 2023 07:04
@@ -114,6 +115,7 @@ private OAuth2ServerConfig readConfiguration(String body)
else {
userinfoEndpoint = Optional.empty();
}
Optional<URI> endSessionEndpoint = Optional.of(getRequiredField("end_session_endpoint", metadata.getEndSessionEndpointURI(), END_SESSION_URL, Optional.empty()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should provide an override value for END_SESSION_URL which comes from the OidcDiscoveryConfig. Similar to the other URL values. This allows non-standard endpoint values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point but OidcDiscoveryConfig actually allows providing urls explicitly (effectively overriding those obtained from metadata discovery) for backward compatibility, allowing for non-standard endpoint values wasn't the objective. We intend to drop these properties #18101
That said, is it something you think we should support? Is this something you use these properties for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This END_SESSION_URL, Optional.empty()) becomes this END_SESSION_URL, logoutUrl), where logoutUrl is set in the constructor

logoutUrl = requireNonNull(oidcConfig.getEndSessionUrl(), "logoutUrl is null");

I had to make these changes for a customer, who is using Ping. The "standard" endpoint found with end_session_endpoint is not being found because the well-known endpoint is returning ping_end_session_endpoint. So the customer had to be able to specify the logout URL explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if we disable the OIDC discovery then we could inject them via StaticConfigurationProvider - we could provide custom end-points right ? cc : @lukasz-walkiewicz Are we removing that endpoint also ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, StaticConfigurationProvider is here to stay. I think we ultimately would like to remove this overriding some day and users should provide configuration explicitly if the IdP does not conform OIDC spec.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thorbjornsen In this case we could still use StaticOAuth2ServerConfiguration but we need to specify the configuration explicitly.

Copy link
Member

@lukasz-walkiewicz lukasz-walkiewicz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM % minor changes to the docs

@@ -175,7 +177,8 @@ public void createClient(
"--response-types", "token,code,id_token",
"--scope", "openid,offline",
"--token-endpoint-auth-method", tokenEndpointAuthMethod.getValue(),
"--callbacks", callbackUrl)
"--callbacks", callbackUrl,
"--post-logout-callbacks", logoutCallbackUrl)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we need to update docs to add information about registering post-logout url

@@ -114,6 +115,7 @@ private OAuth2ServerConfig readConfiguration(String body)
else {
userinfoEndpoint = Optional.empty();
}
Optional<URI> endSessionEndpoint = Optional.of(getRequiredField("end_session_endpoint", metadata.getEndSessionEndpointURI(), END_SESSION_URL, Optional.empty()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point but OidcDiscoveryConfig actually allows providing urls explicitly (effectively overriding those obtained from metadata discovery) for backward compatibility, allowing for non-standard endpoint values wasn't the objective. We intend to drop these properties #18101
That said, is it something you think we should support? Is this something you use these properties for?

@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from 0ac0b57 to a97d69f Compare October 10, 2023 06:20
@Praveen2112
Copy link
Member Author

@mosabua Can you please check the docs.

@Praveen2112
Copy link
Member Author

@lukasz-walkiewicz Updated the docs

@Praveen2112 Praveen2112 requested a review from dain October 10, 2023 06:24
@github-actions github-actions bot added the docs label Oct 10, 2023
@@ -29,14 +29,16 @@ class OAuth2ServerConfig
private final URI tokenUrl;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be record. Maybe follow up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added it as a prep commit

@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from a97d69f to 8309405 Compare October 10, 2023 12:24
Extract cookieExpiration time computation
If the NodeInternalServer is not set, we tend to pick a random interface from getNetworkInterfaces
- Extracted cookie attribute assertion
- Rename assertTrinoCookie to assertTrinoOAuth2Cookie
- Extract parseJwsClaims from TestOAuth2WebUiAuthenticationFilterWithJwt to
 BaseOAuth2WebUiAuthenticationFilterTest
@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from 8309405 to 370f00b Compare October 10, 2023 13:12
docs/src/main/sphinx/security/oauth2.md Outdated Show resolved Hide resolved
docs/src/main/sphinx/security/oauth2.md Outdated Show resolved Hide resolved
docs/src/main/sphinx/security/oauth2.md Outdated Show resolved Hide resolved
@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch 2 times, most recently from 4df11c9 to b904eb0 Compare October 10, 2023 16:09
@Praveen2112 Praveen2112 requested a review from mosabua October 10, 2023 16:12
@Praveen2112
Copy link
Member Author

@mosabua AC

When trying to logout from Trino WebUI, allows the user to be redirected to a specific
end_session_endpoint thereby allowing user to logout from the IdP/OP.

Specification - https://openid.net/specs/openid-connect-rpinitiated-1_0.html
@Praveen2112 Praveen2112 force-pushed the praveen/rp_initiated_logout branch from b904eb0 to e066e29 Compare October 10, 2023 21:42
@Praveen2112
Copy link
Member Author

@mosabua AC

Copy link
Member

@mosabua mosabua left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs look good now.

@Praveen2112 Praveen2112 merged commit 6adfde2 into trinodb:master Oct 11, 2023
69 checks passed
@github-actions github-actions bot added this to the 429 milestone Oct 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

Implement session logout and redirection to login Oauth server
6 participants