mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
Logout confirmation
Signed-off-by: Sebastian Łaskawiec <sebastian.laskawiec@gmail.com>
This commit is contained in:
committed by
Marek Posolda
parent
f6676ccd76
commit
aa789dd023
@@ -60,3 +60,7 @@ Organization administrators can now manage organization invitations through both
|
||||
All invitations are now persistently stored in the database, providing better tracking and management capabilities.
|
||||
|
||||
The invitation management features are available in the *Invitations* tab when managing an organization in the Admin Console, and through the Organizations REST API endpoints under `/admin/realms/{realm}/orgs/{orgId}/invitations`.
|
||||
|
||||
= Logout confirmation
|
||||
|
||||
The client logout configuration page now includes an option to enable logout confirmation. When enabled, users will see "You are logged out" confirmation page upon successful logout.
|
||||
|
||||
@@ -128,3 +128,6 @@ There will be also one item on the consent screen about this client itself.
|
||||
Specifies whether a session ID Claim is included in the Logout Token when the *Backchannel Logout URL* is used.
|
||||
|
||||
*Backchannel logout revoke offline sessions*:: Specifies whether a revoke_offline_access event is included in the Logout Token when the Backchannel Logout URL is used. {project_name} will revoke offline sessions when receiving a Logout Token with this event.
|
||||
|
||||
[[_logout-confirmation]]
|
||||
*Logout confirmation*:: When enabled, {project_name} displays a confirmation page to the user after a successful logout that reads “You are logged out”. This setting primarily affects browser-based logouts, including xref:_oidc-logout[OIDC Logout] initiated by the client (RP-Initiated Logout). If a `post_logout_redirect_uri` is provided and validated for this client, the confirmation page includes a link (or button) to continue to that URL instead of redirecting automatically.
|
||||
|
||||
@@ -340,6 +340,7 @@ This redirect usually happens when the user clicks the `Log Out` link on the pag
|
||||
Once the user is redirected to the logout endpoint, {project_name} is going to send logout requests to
|
||||
clients attached to the current browser SSO session to let them invalidate their local user sessions, and potentially redirect the user to some URL
|
||||
once the logout process is finished. The user might be optionally requested to confirm the logout in case the `id_token_hint` parameter was not used.
|
||||
If the client has xref:_logout-confirmation[Logout confirmation] enabled, {project_name} renders a confirmation page after a successful logout informing the user that they are logged out. When a valid `post_logout_redirect_uri` is provided, this page includes an option to continue to that URL.
|
||||
After logout, the user is automatically redirected to the specified `post_logout_redirect_uri` as long as it is provided as a parameter.
|
||||
Note that you need to include either the `client_id` or `id_token_hint` parameter in case the `post_logout_redirect_uri` is included. Also the `post_logout_redirect_uri` parameter
|
||||
needs to match one of the `Valid Post Logout Redirect URIs` specified in the client configuration.
|
||||
|
||||
@@ -1325,6 +1325,8 @@ generatedAccessTokenHelp=See the example access token, which will be generated a
|
||||
webAuthnPolicyAcceptableAaguidsHelp=The list of allowed AAGUIDs of which an authenticator can be registered. An AAGUID is a 128-bit identifier indicating the authenticator's type (e.g., make and model).
|
||||
keyPasswordHelp=Password for the private key
|
||||
frontchannelLogout=Front channel logout
|
||||
logoutConfirmation=Logout confirmation
|
||||
logoutConfirmationHelp=After logout of the user (OIDC RP-Initiated logout), there will be additional confirmation info page displayed to the user with the text like 'You are logged out' before redirecting a user to a post-logout landing page. On this info page, user needs to confirm that he want to be redirected to the post-logout landing page.
|
||||
clientUpdaterTrustedHostsTooltip=List of Hosts, which are trusted. If that client registration or update request comes from the host/domain specified in this configuration, the condition evaluates to true. You can use hostnames or IP addresses. If you use star at the beginning (for example '*.example.com'), the whole domain example.com is trusted.
|
||||
titleRoles=Realm roles
|
||||
sectorIdentifierUri.tooltip=Providers that use pairwise sub values and support Dynamic Client Registration SHOULD use the sector_identifier_uri parameter. It provides a way for a group of websites under common administrative control to have consistent pairwise sub values independent of the individual domain names. It also provides a way for Clients to change redirect_uri domains without having to reregister all their users.
|
||||
|
||||
@@ -167,6 +167,17 @@ export const LogoutPanel = ({
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
{protocol === "openid-connect" && (
|
||||
<DefaultSwitchControl
|
||||
name={convertAttributeNameToForm<FormFields>(
|
||||
"attributes.logout.confirmation.enabled",
|
||||
)}
|
||||
defaultValue="false"
|
||||
label={t("logoutConfirmation")}
|
||||
labelIcon={t("logoutConfirmationHelp")}
|
||||
stringify
|
||||
/>
|
||||
)}
|
||||
<FixedButtonsGroup
|
||||
name="settings"
|
||||
save={save}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { continueNext, createClient, save } from "./utils.ts";
|
||||
import {
|
||||
assertKeyForCodeExchangeInput,
|
||||
selectKeyForCodeExchangeInput,
|
||||
toggleLogoutConfirmation,
|
||||
} from "./details.ts";
|
||||
|
||||
test.describe.serial("Clients details test", () => {
|
||||
@@ -69,6 +70,7 @@ test.describe.serial("Clients details test", () => {
|
||||
test("Should be able to update a client", async ({ page }) => {
|
||||
await clickTableRowItem(page, clientId);
|
||||
await selectKeyForCodeExchangeInput(page, "S256");
|
||||
await toggleLogoutConfirmation(page);
|
||||
await save(page);
|
||||
await assertNotificationMessage(page, "Client successfully updated");
|
||||
await assertKeyForCodeExchangeInput(page, "S256");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { selectItem, assertSelectValue } from "../utils/form.ts";
|
||||
import { selectItem, assertSelectValue, switchToggle } from "../utils/form.ts";
|
||||
|
||||
function getKeyForCodeExchangeInput(page: Page) {
|
||||
return page.locator("#keyForCodeExchange");
|
||||
@@ -12,3 +12,9 @@ export async function selectKeyForCodeExchangeInput(page: Page, value: string) {
|
||||
export async function assertKeyForCodeExchangeInput(page: Page, value: string) {
|
||||
await assertSelectValue(getKeyForCodeExchangeInput(page), value);
|
||||
}
|
||||
|
||||
export async function toggleLogoutConfirmation(page: Page) {
|
||||
const logoutConfirmationSwitch =
|
||||
"#attributes\\.logout🍺confirmation🍺enabled";
|
||||
await switchToggle(page, logoutConfirmationSwitch);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,8 @@ public final class OIDCConfigAttributes {
|
||||
|
||||
public static final String BACKCHANNEL_LOGOUT_REVOKE_OFFLINE_TOKENS = "backchannel.logout.revoke.offline.tokens";
|
||||
|
||||
public static final String LOGOUT_CONFIRMATION_ENABLED = "logout.confirmation.enabled";
|
||||
|
||||
public static final String USE_REFRESH_TOKEN_FOR_CLIENT_CREDENTIALS_GRANT = "client_credentials.use_refresh_token";
|
||||
|
||||
public static final String USE_REFRESH_TOKEN = "use.refresh.tokens";
|
||||
|
||||
@@ -380,6 +380,14 @@ public class OIDCAdvancedConfigWrapper extends AbstractClientConfigWrapper {
|
||||
setAttribute(OIDCConfigAttributes.TOKEN_ENDPOINT_AUTH_SIGNING_MAX_EXP, String.valueOf(maxExp));
|
||||
}
|
||||
|
||||
public boolean isLogoutConfirmationEnabled() {
|
||||
return Boolean.parseBoolean(getAttribute(OIDCConfigAttributes.LOGOUT_CONFIRMATION_ENABLED, "false"));
|
||||
}
|
||||
|
||||
public void setLogoutConfirmationEnabled(boolean enabled) {
|
||||
setAttribute(OIDCConfigAttributes.LOGOUT_CONFIRMATION_ENABLED, String.valueOf(enabled));
|
||||
}
|
||||
|
||||
public String getBackchannelLogoutUrl() {
|
||||
return getAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import jakarta.ws.rs.core.UriBuilder;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.utils.SystemClientUtil;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
@@ -39,14 +40,20 @@ public class LogoutUtil {
|
||||
|
||||
public static Response sendResponseAfterLogoutFinished(KeycloakSession session, AuthenticationSessionModel logoutSession) {
|
||||
String redirectUri = logoutSession.getAuthNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI);
|
||||
if (redirectUri != null) {
|
||||
URI finalRedirectUri = getRedirectUriWithAttachedState(redirectUri, logoutSession);
|
||||
return Response.status(302).location(finalRedirectUri).build();
|
||||
URI finalRedirectUri = getRedirectUriWithAttachedState(redirectUri, logoutSession);
|
||||
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(logoutSession.getClient());
|
||||
LoginFormsProvider loginFormsProvider = session.getProvider(LoginFormsProvider.class);
|
||||
|
||||
if (finalRedirectUri != null) {
|
||||
if (!config.isLogoutConfirmationEnabled()) {
|
||||
return Response.status(302).location(finalRedirectUri).build();
|
||||
}
|
||||
loginFormsProvider.setAttribute("pageRedirectUri", finalRedirectUri.toString());
|
||||
}
|
||||
|
||||
SystemClientUtil.checkSkipLink(session, logoutSession);
|
||||
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
return loginFormsProvider
|
||||
.setSuccess(Messages.SUCCESS_LOGOUT)
|
||||
.setDetachedAuthSession()
|
||||
.createInfoPage();
|
||||
|
||||
@@ -161,6 +161,35 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
|
||||
assertCurrentUrlEquals(redirectUri + "&state=something");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void logoutRedirectWithLogoutConfirmationEnabled() throws Exception {
|
||||
try (ClientAttributeUpdater ignore = ClientAttributeUpdater.forClient(adminClient, "test", "test-app")
|
||||
.setAttribute(OIDCConfigAttributes.LOGOUT_CONFIRMATION_ENABLED, "true")
|
||||
.update()) {
|
||||
AccessTokenResponse tokenResponse = loginUser();
|
||||
String sessionId = tokenResponse.getSessionState();
|
||||
|
||||
String redirectUri = APP_REDIRECT_URI + "?logout";
|
||||
|
||||
String idTokenString = tokenResponse.getIdToken();
|
||||
|
||||
oauth.logoutForm().postLogoutRedirectUri(redirectUri).idTokenHint(idTokenString).open();
|
||||
|
||||
// Logout should be processed and session terminated
|
||||
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, redirectUri).assertEvent();
|
||||
MatcherAssert.assertThat(false, is(isSessionActive(sessionId)));
|
||||
|
||||
// With logout confirmation enabled, an info page should be shown instead of an immediate redirect
|
||||
infoPage.assertCurrent();
|
||||
Assert.assertEquals("You are logged out", infoPage.getInfo());
|
||||
|
||||
// The info page should contain a link back to application (redirectUri). Use the link to finish.
|
||||
infoPage.clickBackToApplicationLink();
|
||||
WaitUtils.waitForPageToLoad();
|
||||
assertCurrentUrlEquals(redirectUri);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postLogoutRedirect() {
|
||||
AccessTokenResponse tokenResponse = loginUser();
|
||||
|
||||
Reference in New Issue
Block a user