Support for frontchannel_logout_session_required OIDC client parameter (#11009)

* Support for frontchannel_logout_session_required OIDC client parameter
Closes #10137
This commit is contained in:
Marek Posolda 2022-03-31 14:25:24 +02:00 committed by GitHub
parent 7555063ed9
commit aacae9b9ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 102 additions and 3 deletions

View file

@ -144,6 +144,8 @@ public class OIDCClientRepresentation {
private String frontchannel_logout_uri;
private Boolean frontchannel_logout_session_required;
public List<String> getRedirectUris() {
return redirect_uris;
}
@ -569,4 +571,12 @@ public class OIDCClientRepresentation {
public void setFrontChannelLogoutUri(String frontchannel_logout_uri) {
this.frontchannel_logout_uri = frontchannel_logout_uri;
}
public Boolean getFrontchannelLogoutSessionRequired() {
return frontchannel_logout_session_required;
}
public void setFrontchannelLogoutSessionRequired(Boolean frontchannel_logout_session_required) {
this.frontchannel_logout_session_required = frontchannel_logout_session_required;
}
}

View file

@ -72,7 +72,8 @@ public class FrontChannelLogoutHandler {
}
private URI createFrontChannelLogoutUrl(ClientModel client) {
String frontChannelLogoutUrl = OIDCAdvancedConfigWrapper.fromClientModel(client).getFrontChannelLogoutUrl();
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientModel(client);
String frontChannelLogoutUrl = config.getFrontChannelLogoutUrl();
if (StringUtil.isBlank(frontChannelLogoutUrl)) {
frontChannelLogoutUrl = client.getBaseUrl();
@ -84,8 +85,10 @@ public class FrontChannelLogoutHandler {
UriBuilder builder = UriBuilder.fromUri(frontChannelLogoutUrl);
if (config.isFrontChannelLogoutSessionRequired()) {
builder.queryParam("sid", FrontChannelLogoutHandler.this.sid);
builder.queryParam("iss", FrontChannelLogoutHandler.this.issuer);
}
return builder.build();
}

View file

@ -321,6 +321,17 @@ public class OIDCAdvancedConfigWrapper extends AbstractClientConfigWrapper {
return getAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI);
}
public boolean isFrontChannelLogoutSessionRequired() {
String frontChannelLogoutSessionRequired = getAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED);
// Include session by default for backwards compatibility
return frontChannelLogoutSessionRequired == null ? true : Boolean.parseBoolean(frontChannelLogoutSessionRequired);
}
public void setFrontChannelLogoutSessionRequired(boolean frontChannelLogoutSessionRequired) {
String val = String.valueOf(frontChannelLogoutSessionRequired);
setAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, val);
}
public void setLogoUri(String logoUri) {
setAttribute(ClientModel.LOGO_URI, logoUri);
}

View file

@ -78,6 +78,7 @@ public final class OIDCConfigAttributes {
public static final String AUTHORIZATION_ENCRYPTED_RESPONSE_ALG = "authorization.encrypted.response.alg";
public static final String AUTHORIZATION_ENCRYPTED_RESPONSE_ENC = "authorization.encrypted.response.enc";
public static final String FRONT_CHANNEL_LOGOUT_URI = "frontchannel.logout.url";
public static final String FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED = "frontchannel.logout.session.required";
private OIDCConfigAttributes() {
}

View file

@ -234,6 +234,12 @@ public class DescriptionConverter {
}
configWrapper.setFrontChannelLogoutUrl(Optional.ofNullable(clientOIDC.getFrontChannelLogoutUri()).orElse(null));
if (clientOIDC.getFrontchannelLogoutSessionRequired() == null) {
// False by default per OIDC FrontChannel Logout specification
configWrapper.setFrontChannelLogoutSessionRequired(false);
} else {
configWrapper.setFrontChannelLogoutSessionRequired(clientOIDC.getFrontchannelLogoutSessionRequired());
}
if (clientOIDC.getDefaultAcrValues() != null) {
configWrapper.setAttributeMultivalued(Constants.DEFAULT_ACR_VALUES, clientOIDC.getDefaultAcrValues());
@ -419,6 +425,7 @@ public class DescriptionConverter {
}
response.setFrontChannelLogoutUri(config.getFrontChannelLogoutUrl());
response.setFrontchannelLogoutSessionRequired(config.isFrontChannelLogoutSessionRequired());
List<String> defaultAcrValues = config.getAttributeMultivalued(Constants.DEFAULT_ACR_VALUES);
if (!defaultAcrValues.isEmpty()) {

View file

@ -92,6 +92,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
client.setClientUri("http://root");
client.setRedirectUris(Collections.singletonList("http://redirect"));
client.setFrontChannelLogoutUri("http://frontchannel");
client.setFrontchannelLogoutSessionRequired(true);
return client;
}
@ -161,6 +162,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
Assert.assertNull(response.getUserinfoSignedResponseAlg());
assertEquals("http://frontchannel", response.getFrontChannelLogoutUri());
assertTrue(response.getFrontchannelLogoutSessionRequired());
}
@Test
@ -281,6 +283,19 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
Assert.assertNull(kcClientRep.getSecret());
}
@Test
public void createClientFrontchannelLogoutSettings() throws ClientRegistrationException {
// When frontchannelLogutSessionRequired is not set, it should be false by default per OIDC Client registration specification
OIDCClientRepresentation clientRep = createRep();
clientRep.setFrontchannelLogoutSessionRequired(null);
OIDCClientRepresentation response = reg.oidc().create(clientRep);
Assert.assertEquals(false, response.getFrontchannelLogoutSessionRequired());
String clientId = response.getClientId();
ClientRepresentation kcClientRep = getKeycloakClient(clientId);
Assert.assertFalse(OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClientRep).isFrontChannelLogoutSessionRequired());
}
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
@Test

View file

@ -627,6 +627,36 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
}
}
@Test
public void testFrontChannelLogoutWithoutSessionRequired() throws Exception {
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
rep.setFrontchannelLogout(true);
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "false");
clients.get(rep.getId()).update(rep);
try {
oauth.clientSessionState("client-session");
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
String idTokenString = tokenResponse.getIdToken();
String logoutUrl = oauth.getLogoutUrl().idTokenHint(idTokenString)
.postLogoutRedirectUri(oauth.APP_AUTH_ROOT).build();
driver.navigate().to(logoutUrl);
LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken();
Assert.assertNotNull(logoutToken);
Assert.assertNull(logoutToken.getIssuer());
Assert.assertNull(logoutToken.getSid());
} finally {
rep.setFrontchannelLogout(false);
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_SESSION_REQUIRED, "true");
clients.get(rep.getId()).update(rep);
}
}
@Test
public void testFrontChannelLogout() throws Exception {
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();

View file

@ -366,6 +366,8 @@ front-channel-logout=Front Channel Logout
front-channel-logout.tooltip=When true, logout requires a browser redirect to client. When false, server performs a background invocation for logout.
front-channel-logout-url=Front-Channel Logout URL
front-channel-logout-url.tooltip=URL that will cause the client to log itself out when a logout request is sent to this realm (via end_session_endpoint). If not provided, it defaults to the base url.
front-channel-logout-session-required=Front-Channel Logout Session Required
front-channel-logout-session-required.tooltip=Specifying whether a sid (session ID) and iss (Issuer) claims are included in the Logout Token when the Front-Channel Logout URL is used.
force-name-id-format=Force Name ID Format
force-name-id-format.tooltip=Ignore requested NameID subject format and use admin console configured one.

View file

@ -1497,6 +1497,13 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
}
}
if ($scope.client.attributes["frontchannel.logout.session.required"]) {
if ($scope.client.attributes["frontchannel.logout.session.required"] == "true") {
$scope.frontchannelLogoutSessionRequired = true;
} else {
$scope.frontchannelLogoutSessionRequired = false;
}
}
if ($scope.client.attributes["request.uris"] && $scope.client.attributes["request.uris"].length > 0) {
$scope.client.requestUris = $scope.client.attributes["request.uris"].split("##");
@ -2046,6 +2053,12 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.clientEdit.attributes["backchannel.logout.revoke.offline.tokens"] = "false";
}
if ($scope.frontchannelLogoutSessionRequired == true) {
$scope.clientEdit.attributes["frontchannel.logout.session.required"] = "true";
} else {
$scope.clientEdit.attributes["frontchannel.logout.session.required"] = "false";
}
$scope.clientEdit.attributes["acr.loa.map"] = JSON.stringify($scope.acrLoaMap);
$scope.clientEdit.protocol = $scope.protocol;

View file

@ -285,6 +285,13 @@
</div>
<kc-tooltip>{{:: 'front-channel-logout-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && clientEdit.frontchannelLogout">
<label class="col-md-2 control-label" for="frontchannelLogoutSessionRequired">{{:: 'front-channel-logout-session-required' | translate}}</label>
<div class="col-sm-6">
<input ng-model="frontchannelLogoutSessionRequired" name="frontchannelLogoutSessionRequired" id="frontchannelLogoutSessionRequired" onoffswitch ng-click="switchChange()" on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'front-channel-logout-session-required.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlForceNameIdFormat">{{:: 'force-name-id-format' | translate}}</label>
<div class="col-sm-6">