Allow setting if both 'client_id' and 'id_token_hint' params should be sent in logout requests
Closes #27281 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
c18c4bbeb8
commit
0c91fceaad
7 changed files with 152 additions and 4 deletions
|
@ -3065,3 +3065,7 @@ error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at m
|
||||||
multivalued=Multivalued
|
multivalued=Multivalued
|
||||||
multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation
|
multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation
|
||||||
to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
|
to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
|
||||||
|
sendIdTokenOnLogout=Send 'id_token_hint' in logout requests
|
||||||
|
sendIdTokenOnLogoutHelp=If the 'id_token_hint' parameter should be sent in logout requests.
|
||||||
|
sendClientIdOnLogout=Send 'client_id' in logout requests
|
||||||
|
sendClientIdOnLogoutHelp=If the 'client_id' parameter should be sent in logout requests.
|
|
@ -163,6 +163,19 @@ const Fields = ({ readOnly }: DescriptorSettingsProps) => {
|
||||||
data-testid="backchannelLogout"
|
data-testid="backchannelLogout"
|
||||||
isReadOnly={readOnly}
|
isReadOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
<SwitchField
|
||||||
|
field="config.sendIdTokenOnLogout"
|
||||||
|
label="sendIdTokenOnLogout"
|
||||||
|
data-testid="sendIdTokenOnLogout"
|
||||||
|
defaultValue={"true"}
|
||||||
|
isReadOnly={readOnly}
|
||||||
|
/>
|
||||||
|
<SwitchField
|
||||||
|
field="config.sendClientIdOnLogout"
|
||||||
|
label="sendClientIdOnLogout"
|
||||||
|
data-testid="sendClientIdOnLogout"
|
||||||
|
isReadOnly={readOnly}
|
||||||
|
/>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("nameIdPolicyFormat")}
|
label={t("nameIdPolicyFormat")}
|
||||||
labelIcon={
|
labelIcon={
|
||||||
|
|
|
@ -45,6 +45,15 @@ export const ExtendedNonDiscoverySettings = () => {
|
||||||
field="config.backchannelSupported"
|
field="config.backchannelSupported"
|
||||||
label="backchannelLogout"
|
label="backchannelLogout"
|
||||||
/>
|
/>
|
||||||
|
<SwitchField
|
||||||
|
field="config.sendIdTokenOnLogout"
|
||||||
|
label="sendIdTokenOnLogout"
|
||||||
|
defaultValue={"true"}
|
||||||
|
/>
|
||||||
|
<SwitchField
|
||||||
|
field="config.sendClientIdOnLogout"
|
||||||
|
label="sendClientIdOnLogout"
|
||||||
|
/>
|
||||||
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
|
<SwitchField field="config.disableUserInfo" label="disableUserInfo" />
|
||||||
<SwitchField field="config.disableNonce" label="disableNonce" />
|
<SwitchField field="config.disableNonce" label="disableNonce" />
|
||||||
<TextField field="config.defaultScope" label="scopes" />
|
<TextField field="config.defaultScope" label="scopes" />
|
||||||
|
|
|
@ -8,6 +8,7 @@ type FieldType = "boolean" | "string";
|
||||||
|
|
||||||
type SwitchFieldProps = FieldProps & {
|
type SwitchFieldProps = FieldProps & {
|
||||||
fieldType?: FieldType;
|
fieldType?: FieldType;
|
||||||
|
defaultValue?: string | boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SwitchField = ({
|
export const SwitchField = ({
|
||||||
|
@ -15,6 +16,7 @@ export const SwitchField = ({
|
||||||
field,
|
field,
|
||||||
fieldType = "string",
|
fieldType = "string",
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
|
defaultValue,
|
||||||
}: SwitchFieldProps) => {
|
}: SwitchFieldProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
|
@ -22,7 +24,9 @@ export const SwitchField = ({
|
||||||
<FormGroupField label={label}>
|
<FormGroupField label={label}>
|
||||||
<Controller
|
<Controller
|
||||||
name={field}
|
name={field}
|
||||||
defaultValue={fieldType === "string" ? "false" : false}
|
defaultValue={
|
||||||
|
defaultValue ? defaultValue : fieldType === "string" ? "false" : false
|
||||||
|
}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
@ -142,7 +142,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
String sessionId = userSession.getId();
|
String sessionId = userSession.getId();
|
||||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||||
.queryParam("state", sessionId);
|
.queryParam("state", sessionId);
|
||||||
logoutUri.queryParam("id_token_hint", idToken);
|
if (getConfig().isSendIdTokenOnLogout() && idToken != null) {
|
||||||
|
logoutUri.queryParam("id_token_hint", idToken);
|
||||||
|
}
|
||||||
|
if (getConfig().isSendClientIdOnLogout()) {
|
||||||
|
logoutUri.queryParam("client_id", getConfig().getClientId());
|
||||||
|
}
|
||||||
String url = logoutUri.build().toString();
|
String url = logoutUri.build().toString();
|
||||||
try {
|
try {
|
||||||
int status = SimpleHttp.doGet(url, session).asStatus();
|
int status = SimpleHttp.doGet(url, session).asStatus();
|
||||||
|
@ -160,14 +165,19 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||||
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
|
||||||
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
|
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
|
||||||
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
||||||
if (idToken != null && getConfig().isBackchannelSupported()) {
|
if (getConfig().isBackchannelSupported()) {
|
||||||
backchannelLogout(userSession, idToken);
|
backchannelLogout(userSession, idToken);
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
String sessionId = userSession.getId();
|
String sessionId = userSession.getId();
|
||||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||||
.queryParam("state", sessionId);
|
.queryParam("state", sessionId);
|
||||||
if (idToken != null) logoutUri.queryParam("id_token_hint", idToken);
|
if (getConfig().isSendIdTokenOnLogout() && idToken != null) {
|
||||||
|
logoutUri.queryParam("id_token_hint", idToken);
|
||||||
|
}
|
||||||
|
if (getConfig().isSendClientIdOnLogout()) {
|
||||||
|
logoutUri.queryParam("client_id", getConfig().getClientId());
|
||||||
|
}
|
||||||
String redirect = RealmsResource.brokerUrl(uriInfo)
|
String redirect = RealmsResource.brokerUrl(uriInfo)
|
||||||
.path(IdentityBrokerService.class, "getEndpoint")
|
.path(IdentityBrokerService.class, "getEndpoint")
|
||||||
.path(OIDCEndpoint.class, "logoutResponse")
|
.path(OIDCEndpoint.class, "logoutResponse")
|
||||||
|
|
|
@ -61,6 +61,22 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
||||||
getConfig().put("logoutUrl", url);
|
getConfig().put("logoutUrl", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSendClientIdOnLogout() {
|
||||||
|
return Boolean.parseBoolean(getConfig().getOrDefault("sendClientIdOnLogout", Boolean.FALSE.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSendClientOnLogout(boolean value) {
|
||||||
|
getConfig().put("sendClientIdOnLogout", Boolean.valueOf(value).toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSendIdTokenOnLogout() {
|
||||||
|
return Boolean.parseBoolean(getConfig().getOrDefault("sendIdTokenOnLogout", Boolean.TRUE.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSendIdTokenOnLogout(boolean value) {
|
||||||
|
getConfig().put("sendIdTokenOnLogout", Boolean.valueOf(value).toString());
|
||||||
|
}
|
||||||
|
|
||||||
public String getPublicKeySignatureVerifier() {
|
public String getPublicKeySignatureVerifier() {
|
||||||
return getConfig().get("publicKeySignatureVerifier");
|
return getConfig().get("publicKeySignatureVerifier");
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,12 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
|
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||||
|
import org.keycloak.admin.client.resource.RealmResource;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.cookie.CookieType;
|
import org.keycloak.cookie.CookieType;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.util.AccountHelper;
|
import org.keycloak.testsuite.util.AccountHelper;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
@ -17,6 +20,10 @@ import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
|
||||||
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
|
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
|
||||||
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
|
public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
|
@ -139,4 +146,89 @@ public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
|
||||||
|
|
||||||
waitForPage(driver, "sign in to provider", true);
|
waitForPage(driver, "sign in to provider", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFrontChannelLogoutRequestsSendingOnlyClientId() {
|
||||||
|
RealmResource realm = adminClient.realm(bc.consumerRealmName());
|
||||||
|
IdentityProviderResource identityProviderResource = realm.identityProviders().get(bc.getIDPAlias());
|
||||||
|
IdentityProviderRepresentation representation = identityProviderResource.toRepresentation();
|
||||||
|
Map<String, String> config = representation.getConfig();
|
||||||
|
Map<String, String> originalConfig = new HashMap<>(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
config.put("backchannelSupported", Boolean.FALSE.toString());
|
||||||
|
config.put("sendIdTokenOnLogout", Boolean.FALSE.toString());
|
||||||
|
config.put("sendClientIdOnLogout", Boolean.TRUE.toString());
|
||||||
|
identityProviderResource.update(representation);
|
||||||
|
logInAsUserInIDPForFirstTime();
|
||||||
|
appPage.assertCurrent();
|
||||||
|
driver.manage().timeouts().pageLoadTimeout(1, TimeUnit.DAYS);
|
||||||
|
executeLogoutFromRealm(
|
||||||
|
getConsumerRoot(),
|
||||||
|
bc.consumerRealmName(),
|
||||||
|
"something-else",
|
||||||
|
null,
|
||||||
|
"account",
|
||||||
|
getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/account"
|
||||||
|
);
|
||||||
|
logoutConfirmPage.isCurrent();
|
||||||
|
// confirm logout at consumer
|
||||||
|
logoutConfirmPage.confirmLogout();
|
||||||
|
// confirm logout at provider
|
||||||
|
logoutConfirmPage.confirmLogout();
|
||||||
|
oauth.clientId("account");
|
||||||
|
oauth.redirectUri(getConsumerRoot() + "/auth/realms/" + REALM_PROV_NAME + "/account");
|
||||||
|
loginPage.open(REALM_PROV_NAME);
|
||||||
|
waitForPage(driver, "sign in to provider", true);
|
||||||
|
} finally {
|
||||||
|
representation.setConfig(originalConfig);
|
||||||
|
identityProviderResource.update(representation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFrontChannelLogoutRequestsSendingOnlyIdTokenHint() throws VerificationException {
|
||||||
|
RealmResource realm = adminClient.realm(bc.consumerRealmName());
|
||||||
|
IdentityProviderResource identityProviderResource = realm.identityProviders().get(bc.getIDPAlias());
|
||||||
|
IdentityProviderRepresentation representation = identityProviderResource.toRepresentation();
|
||||||
|
Map<String, String> config = representation.getConfig();
|
||||||
|
Map<String, String> originalConfig = new HashMap<>(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
config.put("backchannelSupported", Boolean.FALSE.toString());
|
||||||
|
config.put("sendIdTokenOnLogout", Boolean.TRUE.toString());
|
||||||
|
config.put("sendClientIdOnLogout", Boolean.FALSE.toString());
|
||||||
|
driver.navigate().to(getLoginUrl(getConsumerRoot(), bc.consumerRealmName(), "broker-app"));
|
||||||
|
logInWithBroker(bc);
|
||||||
|
updateAccountInformation();
|
||||||
|
|
||||||
|
// Exchange code from "broker-app" client of "consumer" realm for the tokens
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
OAuthClient.AccessTokenResponse response = oauth.realm(bc.consumerRealmName())
|
||||||
|
.clientId("broker-app")
|
||||||
|
.redirectUri(getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app")
|
||||||
|
.doAccessTokenRequest(code, "broker-app-secret");
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
|
||||||
|
String idTokenString = response.getIdToken();
|
||||||
|
|
||||||
|
logoutFromRealm(
|
||||||
|
getConsumerRoot(),
|
||||||
|
bc.consumerRealmName(),
|
||||||
|
"something-else",
|
||||||
|
idTokenString,
|
||||||
|
null,
|
||||||
|
getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app"
|
||||||
|
);
|
||||||
|
|
||||||
|
// user should be logged out successfully from the IDP
|
||||||
|
oauth.clientId(bc.getIDPClientIdInProviderRealm());
|
||||||
|
oauth.redirectUri(BrokerTestTools.getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/broker/" + bc.getIDPAlias() + "/endpoint/*");
|
||||||
|
loginPage.open(REALM_PROV_NAME);
|
||||||
|
waitForPage(driver, "sign in to provider", true);
|
||||||
|
} finally {
|
||||||
|
representation.setConfig(originalConfig);
|
||||||
|
identityProviderResource.update(representation);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue