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
|
||||
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.
|
||||
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"
|
||||
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
|
||||
label={t("nameIdPolicyFormat")}
|
||||
labelIcon={
|
||||
|
|
|
@ -45,6 +45,15 @@ export const ExtendedNonDiscoverySettings = () => {
|
|||
field="config.backchannelSupported"
|
||||
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.disableNonce" label="disableNonce" />
|
||||
<TextField field="config.defaultScope" label="scopes" />
|
||||
|
|
|
@ -8,6 +8,7 @@ type FieldType = "boolean" | "string";
|
|||
|
||||
type SwitchFieldProps = FieldProps & {
|
||||
fieldType?: FieldType;
|
||||
defaultValue?: string | boolean;
|
||||
};
|
||||
|
||||
export const SwitchField = ({
|
||||
|
@ -15,6 +16,7 @@ export const SwitchField = ({
|
|||
field,
|
||||
fieldType = "string",
|
||||
isReadOnly = false,
|
||||
defaultValue,
|
||||
}: SwitchFieldProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { control } = useFormContext();
|
||||
|
@ -22,7 +24,9 @@ export const SwitchField = ({
|
|||
<FormGroupField label={label}>
|
||||
<Controller
|
||||
name={field}
|
||||
defaultValue={fieldType === "string" ? "false" : false}
|
||||
defaultValue={
|
||||
defaultValue ? defaultValue : fieldType === "string" ? "false" : false
|
||||
}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
|
|
|
@ -142,7 +142,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
String sessionId = userSession.getId();
|
||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||
.queryParam("state", sessionId);
|
||||
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();
|
||||
try {
|
||||
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) {
|
||||
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
|
||||
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
|
||||
if (idToken != null && getConfig().isBackchannelSupported()) {
|
||||
if (getConfig().isBackchannelSupported()) {
|
||||
backchannelLogout(userSession, idToken);
|
||||
return null;
|
||||
} else {
|
||||
String sessionId = userSession.getId();
|
||||
UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
|
||||
.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)
|
||||
.path(IdentityBrokerService.class, "getEndpoint")
|
||||
.path(OIDCEndpoint.class, "logoutResponse")
|
||||
|
|
|
@ -61,6 +61,22 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
|
|||
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() {
|
||||
return getConfig().get("publicKeySignatureVerifier");
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
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.cookie.CookieType;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
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.waitForPage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
|
||||
|
||||
@Rule
|
||||
|
@ -139,4 +146,89 @@ public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
|
|||
|
||||
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