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:
parent
7555063ed9
commit
aacae9b9ac
10 changed files with 102 additions and 3 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue