[KEYCLOAK-17653] - OIDC Frontchannel logout support
This commit is contained in:
parent
97ee8832a3
commit
891c8e1a12
29 changed files with 646 additions and 15 deletions
|
@ -49,6 +49,12 @@ public class OIDCConfigurationRepresentation {
|
||||||
@JsonProperty("end_session_endpoint")
|
@JsonProperty("end_session_endpoint")
|
||||||
private String logoutEndpoint;
|
private String logoutEndpoint;
|
||||||
|
|
||||||
|
@JsonProperty("frontchannel_logout_session_supported")
|
||||||
|
private Boolean frontChannelLogoutSessionSupported = true;
|
||||||
|
|
||||||
|
@JsonProperty("frontchannel_logout_supported")
|
||||||
|
private Boolean frontChannelLogoutSupported = true;
|
||||||
|
|
||||||
@JsonProperty("jwks_uri")
|
@JsonProperty("jwks_uri")
|
||||||
private String jwksUri;
|
private String jwksUri;
|
||||||
|
|
||||||
|
@ -577,4 +583,12 @@ public class OIDCConfigurationRepresentation {
|
||||||
public void setAuthorizationEncryptionEncValuesSupported(List<String> authorizationEncryptionEncValuesSupported) {
|
public void setAuthorizationEncryptionEncValuesSupported(List<String> authorizationEncryptionEncValuesSupported) {
|
||||||
this.authorizationEncryptionEncValuesSupported = authorizationEncryptionEncValuesSupported;
|
this.authorizationEncryptionEncValuesSupported = authorizationEncryptionEncValuesSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getFrontChannelLogoutSessionSupported() {
|
||||||
|
return frontChannelLogoutSessionSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getFrontChannelLogoutSupported() {
|
||||||
|
return frontChannelLogoutSupported;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,8 @@ public class OIDCClientRepresentation {
|
||||||
// PAR request
|
// PAR request
|
||||||
private Boolean require_pushed_authorization_requests;
|
private Boolean require_pushed_authorization_requests;
|
||||||
|
|
||||||
|
private String frontchannel_logout_uri;
|
||||||
|
|
||||||
public List<String> getRedirectUris() {
|
public List<String> getRedirectUris() {
|
||||||
return redirect_uris;
|
return redirect_uris;
|
||||||
}
|
}
|
||||||
|
@ -559,4 +561,12 @@ public class OIDCClientRepresentation {
|
||||||
public void setRequirePushedAuthorizationRequests(Boolean require_pushed_authorization_requests) {
|
public void setRequirePushedAuthorizationRequests(Boolean require_pushed_authorization_requests) {
|
||||||
this.require_pushed_authorization_requests = require_pushed_authorization_requests;
|
this.require_pushed_authorization_requests = require_pushed_authorization_requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFrontChannelLogoutUri() {
|
||||||
|
return frontchannel_logout_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFrontChannelLogoutUri(String frontchannel_logout_uri) {
|
||||||
|
this.frontchannel_logout_uri = frontchannel_logout_uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ public enum LoginFormsPages {
|
||||||
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
|
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
|
||||||
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, REGISTER_USER_PROFILE, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
|
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, REGISTER_USER_PROFILE, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
|
||||||
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
|
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
|
||||||
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE;
|
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE,
|
||||||
|
FRONTCHANNEL_LOGOUT;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,8 @@ public interface LoginFormsProvider extends Provider {
|
||||||
|
|
||||||
Response createSamlPostForm();
|
Response createSamlPostForm();
|
||||||
|
|
||||||
|
Response createFrontChannelLogoutPage();
|
||||||
|
|
||||||
LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession);
|
LoginFormsProvider setAuthenticationSession(AuthenticationSessionModel authenticationSession);
|
||||||
|
|
||||||
LoginFormsProvider setClientSessionCode(String accessCode);
|
LoginFormsProvider setClientSessionCode(String accessCode);
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.forms.login.freemarker.model.CodeBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean;
|
import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.LoginBean;
|
import org.keycloak.forms.login.freemarker.model.LoginBean;
|
||||||
|
import org.keycloak.forms.login.freemarker.model.FrontChannelLogoutBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.OAuthGrantBean;
|
import org.keycloak.forms.login.freemarker.model.OAuthGrantBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.ProfileBean;
|
import org.keycloak.forms.login.freemarker.model.ProfileBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.RealmBean;
|
import org.keycloak.forms.login.freemarker.model.RealmBean;
|
||||||
|
@ -187,7 +188,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
|
|
||||||
@SuppressWarnings("incomplete-switch")
|
@SuppressWarnings("incomplete-switch")
|
||||||
protected Response createResponse(LoginFormsPages page) {
|
protected Response createResponse(LoginFormsPages page) {
|
||||||
|
|
||||||
Theme theme;
|
Theme theme;
|
||||||
try {
|
try {
|
||||||
theme = getTheme();
|
theme = getTheme();
|
||||||
|
@ -265,6 +266,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
UpdateProfileContext idpCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
|
UpdateProfileContext idpCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
|
||||||
attributes.put("profile", new IdpReviewProfileBean(idpCtx, formData, session));
|
attributes.put("profile", new IdpReviewProfileBean(idpCtx, formData, session));
|
||||||
break;
|
break;
|
||||||
|
case FRONTCHANNEL_LOGOUT:
|
||||||
|
attributes.put("logout", new FrontChannelLogoutBean(session));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return processTemplate(theme, Templates.getTemplate(page), locale);
|
return processTemplate(theme, Templates.getTemplate(page), locale);
|
||||||
|
@ -273,7 +277,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
private boolean isDynamicUserProfile() {
|
private boolean isDynamicUserProfile() {
|
||||||
return session.getProvider(UserProfileProvider.class).getConfiguration() != null;
|
return session.getProvider(UserProfileProvider.class).getConfiguration() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response createForm(String form) {
|
public Response createForm(String form) {
|
||||||
Theme theme;
|
Theme theme;
|
||||||
|
@ -565,7 +569,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
if(userCtx != null && userCtx.getUserProfileContext() == UserProfileContext.IDP_REVIEW)
|
if(userCtx != null && userCtx.getUserProfileContext() == UserProfileContext.IDP_REVIEW)
|
||||||
return createResponse(LoginFormsPages.IDP_REVIEW_USER_PROFILE);
|
return createResponse(LoginFormsPages.IDP_REVIEW_USER_PROFILE);
|
||||||
else
|
else
|
||||||
return createResponse(LoginFormsPages.UPDATE_USER_PROFILE);
|
return createResponse(LoginFormsPages.UPDATE_USER_PROFILE);
|
||||||
} else {
|
} else {
|
||||||
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
|
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
|
||||||
}
|
}
|
||||||
|
@ -636,6 +640,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
return createResponse(LoginFormsPages.SAML_POST_FORM);
|
return createResponse(LoginFormsPages.SAML_POST_FORM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response createFrontChannelLogoutPage() {
|
||||||
|
return createResponse(LoginFormsPages.FRONTCHANNEL_LOGOUT);
|
||||||
|
}
|
||||||
|
|
||||||
protected void setMessage(MessageType type, String message, Object... parameters) {
|
protected void setMessage(MessageType type, String message, Object... parameters) {
|
||||||
messageType = type;
|
messageType = type;
|
||||||
messages = new ArrayList<>();
|
messages = new ArrayList<>();
|
||||||
|
|
|
@ -78,6 +78,8 @@ public class Templates {
|
||||||
return "update-user-profile.ftl";
|
return "update-user-profile.ftl";
|
||||||
case IDP_REVIEW_USER_PROFILE:
|
case IDP_REVIEW_USER_PROFILE:
|
||||||
return "idp-review-user-profile.ftl";
|
return "idp-review-user-profile.ftl";
|
||||||
|
case FRONTCHANNEL_LOGOUT:
|
||||||
|
return "frontchannel-logout.ftl";
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package org.keycloak.forms.login.freemarker.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.FrontChannelLogoutHandler;
|
||||||
|
|
||||||
|
public class FrontChannelLogoutBean {
|
||||||
|
|
||||||
|
private final FrontChannelLogoutHandler logoutInfo;
|
||||||
|
|
||||||
|
public FrontChannelLogoutBean(KeycloakSession session) {
|
||||||
|
logoutInfo = FrontChannelLogoutHandler.current(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogoutRedirectUri() {
|
||||||
|
return logoutInfo.getLogoutRedirectUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FrontChannelLogoutHandler.ClientInfo> getClients() {
|
||||||
|
return logoutInfo.getClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.headers.SecurityHeadersProvider;
|
||||||
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
|
public class FrontChannelLogoutHandler {
|
||||||
|
|
||||||
|
public static FrontChannelLogoutHandler current(KeycloakSession session) {
|
||||||
|
return (FrontChannelLogoutHandler) session.getAttribute(FrontChannelLogoutHandler.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FrontChannelLogoutHandler currentOrCreate(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
|
||||||
|
FrontChannelLogoutHandler current = current(session);
|
||||||
|
|
||||||
|
if (current == null) {
|
||||||
|
return new FrontChannelLogoutHandler(session, clientSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final String sid;
|
||||||
|
private final String issuer;
|
||||||
|
private final List<ClientInfo> clients = new ArrayList<>();
|
||||||
|
|
||||||
|
private String logoutRedirectUri;
|
||||||
|
|
||||||
|
private FrontChannelLogoutHandler(KeycloakSession session, AuthenticatedClientSessionModel clientSession) {
|
||||||
|
this.session = session;
|
||||||
|
this.sid = clientSession.getUserSession().getId();
|
||||||
|
this.issuer = clientSession.getNote(OIDCLoginProtocol.ISSUER);
|
||||||
|
this.session.setAttribute(getClass().getName(), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addClient(ClientModel client) {
|
||||||
|
clients.add(new ClientInfo(client));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClientInfo> getClients() {
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLogoutRedirectUri() {
|
||||||
|
return logoutRedirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response renderLogoutPage(String redirectUri) {
|
||||||
|
configureCSP();
|
||||||
|
this.logoutRedirectUri = redirectUri;
|
||||||
|
return session.getProvider(LoginFormsProvider.class).createFrontChannelLogoutPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureCSP() {
|
||||||
|
StringBuilder allowFrameSrc = new StringBuilder();
|
||||||
|
|
||||||
|
for (ClientInfo client : clients) {
|
||||||
|
allowFrameSrc.append(client.frontChannelLogoutUrl.getAuthority()).append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
session.getProvider(SecurityHeadersProvider.class).options().allowAnyFrameAncestor();
|
||||||
|
session.getProvider(SecurityHeadersProvider.class).options().allowFrameSrc(allowFrameSrc.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI createFrontChannelLogoutUrl(ClientModel client) {
|
||||||
|
String frontChannelLogoutUrl = OIDCAdvancedConfigWrapper.fromClientModel(client).getFrontChannelLogoutUrl();
|
||||||
|
|
||||||
|
if (StringUtil.isBlank(frontChannelLogoutUrl)) {
|
||||||
|
frontChannelLogoutUrl = client.getBaseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frontChannelLogoutUrl == null) {
|
||||||
|
throw new RuntimeException("Client [" + client.getClientId() + "] does not have a valid frontend logout URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
UriBuilder builder = UriBuilder.fromUri(frontChannelLogoutUrl);
|
||||||
|
|
||||||
|
builder.queryParam("sid", FrontChannelLogoutHandler.this.sid);
|
||||||
|
builder.queryParam("iss", FrontChannelLogoutHandler.this.issuer);
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClientInfo {
|
||||||
|
|
||||||
|
private final ClientModel client;
|
||||||
|
private final URI frontChannelLogoutUrl;
|
||||||
|
|
||||||
|
public ClientInfo(ClientModel client) {
|
||||||
|
this.client = client;
|
||||||
|
this.frontChannelLogoutUrl = createFrontChannelLogoutUrl(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFrontChannelLogoutUrl() {
|
||||||
|
return frontChannelLogoutUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
String name = client.getName();
|
||||||
|
|
||||||
|
if (name == null) {
|
||||||
|
return client.getClientId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import org.keycloak.jose.jws.Algorithm;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -287,6 +288,24 @@ public class OIDCAdvancedConfigWrapper {
|
||||||
setAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_REVOKE_OFFLINE_TOKENS, val);
|
setAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_REVOKE_OFFLINE_TOKENS, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFrontChannelLogoutUrl(String frontChannelLogoutUrl) {
|
||||||
|
if (clientRep != null) {
|
||||||
|
clientRep.setFrontchannelLogout(StringUtil.isNotBlank(frontChannelLogoutUrl));
|
||||||
|
}
|
||||||
|
if (clientModel != null) {
|
||||||
|
clientModel.setFrontchannelLogout(StringUtil.isNotBlank(frontChannelLogoutUrl));
|
||||||
|
}
|
||||||
|
setAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, frontChannelLogoutUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFrontChannelLogoutEnabled() {
|
||||||
|
return clientModel != null && clientModel.isFrontchannelLogout() && StringUtil.isNotBlank(getFrontChannelLogoutUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFrontChannelLogoutUrl() {
|
||||||
|
return getAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI);
|
||||||
|
}
|
||||||
|
|
||||||
private String getAttribute(String attrKey) {
|
private String getAttribute(String attrKey) {
|
||||||
if (clientModel != null) {
|
if (clientModel != null) {
|
||||||
return clientModel.getAttribute(attrKey);
|
return clientModel.getAttribute(attrKey);
|
||||||
|
|
|
@ -75,6 +75,7 @@ public final class OIDCConfigAttributes {
|
||||||
public static final String AUTHORIZATION_SIGNED_RESPONSE_ALG = "authorization.signed.response.alg";
|
public static final String AUTHORIZATION_SIGNED_RESPONSE_ALG = "authorization.signed.response.alg";
|
||||||
public static final String AUTHORIZATION_ENCRYPTED_RESPONSE_ALG = "authorization.encrypted.response.alg";
|
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 AUTHORIZATION_ENCRYPTED_RESPONSE_ENC = "authorization.encrypted.response.enc";
|
||||||
|
public static final String FRONT_CHANNEL_LOGOUT_URI = "frontchannel.logout.url";
|
||||||
|
|
||||||
private OIDCConfigAttributes() {
|
private OIDCConfigAttributes() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.constants.AdapterConstants;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.headers.SecurityHeadersProvider;
|
import org.keycloak.headers.SecurityHeadersProvider;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
|
@ -40,7 +41,6 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
|
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
import org.keycloak.protocol.oidc.utils.OIDCRedirectUriBuilder;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
|
@ -338,8 +338,15 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
|
||||||
// todo oidc redirect support
|
if (clientSession != null) {
|
||||||
throw new RuntimeException("NOT IMPLEMENTED");
|
ClientModel client = clientSession.getClient();
|
||||||
|
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isFrontChannelLogoutEnabled()) {
|
||||||
|
FrontChannelLogoutHandler logoutInfo = FrontChannelLogoutHandler.currentOrCreate(session, clientSession);
|
||||||
|
logoutInfo.addClient(client);
|
||||||
|
}
|
||||||
|
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -351,7 +358,10 @@ public class OIDCLoginProtocol implements LoginProtocol {
|
||||||
event.detail(Details.REDIRECT_URI, redirectUri);
|
event.detail(Details.REDIRECT_URI, redirectUri);
|
||||||
}
|
}
|
||||||
event.user(userSession.getUser()).session(userSession).success();
|
event.user(userSession.getUser()).session(userSession).success();
|
||||||
|
FrontChannelLogoutHandler frontChannelLogoutHandler = FrontChannelLogoutHandler.current(session);
|
||||||
|
if (frontChannelLogoutHandler != null) {
|
||||||
|
return frontChannelLogoutHandler.renderLogoutPage(redirectUri);
|
||||||
|
}
|
||||||
if (redirectUri != null) {
|
if (redirectUri != null) {
|
||||||
UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
|
UriBuilder uriBuilder = UriBuilder.fromUri(redirectUri);
|
||||||
if (state != null)
|
if (state != null)
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.services.clientpolicy.context;
|
package org.keycloak.services.clientpolicy.context;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedHashMap;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
|
||||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||||
|
@ -33,6 +34,10 @@ public class LogoutRequestContext implements ClientPolicyContext {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LogoutRequestContext() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientPolicyEvent getEvent() {
|
public ClientPolicyEvent getEvent() {
|
||||||
return ClientPolicyEvent.LOGOUT_REQUEST;
|
return ClientPolicyEvent.LOGOUT_REQUEST;
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientpolicy.executor;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import javax.ws.rs.HttpMethod;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
|
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||||
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
|
import org.keycloak.services.clientpolicy.context.ClientCRUDContext;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class SecureLogoutExecutor implements ClientPolicyExecutorProvider<SecureLogoutExecutor.Configuration> {
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private Configuration configuration;
|
||||||
|
|
||||||
|
public SecureLogoutExecutor(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setupConfiguration(Configuration config) {
|
||||||
|
this.configuration = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Configuration> getExecutorConfigurationClass() {
|
||||||
|
return Configuration.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
|
||||||
|
@JsonProperty(SecureLogoutExecutorFactory.ALLOW_FRONT_CHANNEL_LOGOUT)
|
||||||
|
protected Boolean allowFrontChannelLogout = Boolean.FALSE;
|
||||||
|
|
||||||
|
public Boolean isAllowFrontChannelLogout() {
|
||||||
|
return allowFrontChannelLogout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowFrontChannelLogout(Boolean allowFrontChannelLogout) {
|
||||||
|
this.allowFrontChannelLogout = allowFrontChannelLogout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderId() {
|
||||||
|
return PKCEEnforcerExecutorFactory.PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||||
|
switch (context.getEvent()) {
|
||||||
|
case REGISTER:
|
||||||
|
case UPDATE:
|
||||||
|
ClientCRUDContext updateContext = (ClientCRUDContext)context;
|
||||||
|
ClientRepresentation client = updateContext.getProposedClientRepresentation();
|
||||||
|
OIDCAdvancedConfigWrapper clientWrapper = OIDCAdvancedConfigWrapper.fromClientRepresentation(client);
|
||||||
|
|
||||||
|
if (!configuration.isAllowFrontChannelLogout()
|
||||||
|
&& (Optional.ofNullable(client.isFrontchannelLogout()).orElse(false) || StringUtil.isNotBlank(clientWrapper.getFrontChannelLogoutUrl()))) {
|
||||||
|
throwFrontChannelLogoutNotAllowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
case LOGOUT_REQUEST:
|
||||||
|
HttpRequest request = session.getContext().getContextObject(HttpRequest.class);
|
||||||
|
|
||||||
|
if (HttpMethod.GET.equalsIgnoreCase(request.getHttpMethod()) && !configuration.isAllowFrontChannelLogout()) {
|
||||||
|
throwFrontChannelLogoutNotAllowed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void throwFrontChannelLogoutNotAllowed() throws ClientPolicyException {
|
||||||
|
throw new ClientPolicyException(Errors.INVALID_REGISTRATION, "Front-channel logout is not allowed for this client");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientpolicy.executor;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
|
public class SecureLogoutExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "secure-logout";
|
||||||
|
|
||||||
|
public static final String ALLOW_FRONT_CHANNEL_LOGOUT = "allow-front-channel-logout";
|
||||||
|
|
||||||
|
private static final ProviderConfigProperty ALLOW_FRONT_CHANNEL_LOGOUT_PROPERTY = new ProviderConfigProperty(
|
||||||
|
ALLOW_FRONT_CHANNEL_LOGOUT, "Allow Front-Channel Logout", "If On, then front-channel logout should be allowed. Otherwise, clients should favor other logout mechanisms such as back-channel logout.", ProviderConfigProperty.BOOLEAN_TYPE, false);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||||
|
return new SecureLogoutExecutor(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Enforces certain constraints on how clients should support logout.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.singletonList(ALLOW_FRONT_CHANNEL_LOGOUT_PROPERTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import org.keycloak.authentication.ClientAuthenticator;
|
||||||
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||||
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
|
||||||
import org.keycloak.crypto.ClientSignatureVerifierProvider;
|
|
||||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||||
import org.keycloak.jose.jwk.JWK;
|
import org.keycloak.jose.jwk.JWK;
|
||||||
import org.keycloak.jose.jwk.JWKParser;
|
import org.keycloak.jose.jwk.JWKParser;
|
||||||
|
@ -66,7 +65,6 @@ import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.keycloak.models.CibaConfig.CIBA_POLL_MODE;
|
|
||||||
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
|
import static org.keycloak.models.OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED;
|
||||||
import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED;
|
import static org.keycloak.models.CibaConfig.OIDC_CIBA_GRANT_ENABLED;
|
||||||
|
|
||||||
|
@ -218,6 +216,8 @@ public class DescriptionConverter {
|
||||||
client.setAttributes(attr);
|
client.setAttributes(attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configWrapper.setFrontChannelLogoutUrl(Optional.ofNullable(clientOIDC.getFrontChannelLogoutUri()).orElse(null));
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,6 +396,8 @@ public class DescriptionConverter {
|
||||||
response.setSectorIdentifierUri(sectorIdentifierUri);
|
response.setSectorIdentifierUri(sectorIdentifierUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response.setFrontChannelLogoutUri(config.getFrontChannelLogoutUrl());
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,8 +70,11 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
|
import org.keycloak.services.clientpolicy.context.LogoutRequestContext;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.IdentityBrokerService;
|
import org.keycloak.services.resources.IdentityBrokerService;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
|
@ -493,6 +496,12 @@ public class AuthenticationManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.clientPolicy().triggerOnEvent(new LogoutRequestContext());
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGING_OUT);
|
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGING_OUT);
|
||||||
|
|
||||||
|
|
|
@ -12,4 +12,5 @@ org.keycloak.services.clientpolicy.executor.ConsentRequiredExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutorFactory
|
org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutorFactory
|
||||||
org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSessionEnforceExecutorFactory
|
org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSessionEnforceExecutorFactory
|
||||||
org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutorFactory
|
org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaSignedAuthenticationRequestExecutorFactory
|
||||||
org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaAuthenticationRequestSigningAlgorithmExecutorFactory
|
org.keycloak.protocol.oidc.grants.ciba.clientpolicy.executor.SecureCibaAuthenticationRequestSigningAlgorithmExecutorFactory
|
||||||
|
org.keycloak.services.clientpolicy.executor.SecureLogoutExecutorFactory
|
|
@ -59,6 +59,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
|
|
||||||
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
private final BlockingQueue<LogoutAction> adminLogoutActions;
|
||||||
|
private final BlockingQueue<LogoutToken> frontChannelLogoutTokens;
|
||||||
private final BlockingQueue<LogoutToken> backChannelLogoutTokens;
|
private final BlockingQueue<LogoutToken> backChannelLogoutTokens;
|
||||||
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
private final BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions;
|
||||||
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
private final BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction;
|
||||||
|
@ -72,6 +73,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
|
|
||||||
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
public TestApplicationResourceProvider(KeycloakSession session, BlockingQueue<LogoutAction> adminLogoutActions,
|
||||||
BlockingQueue<LogoutToken> backChannelLogoutTokens,
|
BlockingQueue<LogoutToken> backChannelLogoutTokens,
|
||||||
|
BlockingQueue<LogoutToken> frontChannelLogoutTokens,
|
||||||
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
BlockingQueue<PushNotBeforeAction> adminPushNotBeforeActions,
|
||||||
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
BlockingQueue<TestAvailabilityAction> adminTestAvailabilityAction,
|
||||||
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
TestApplicationResourceProviderFactory.OIDCClientData oidcClientData,
|
||||||
|
@ -80,6 +82,7 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.adminLogoutActions = adminLogoutActions;
|
this.adminLogoutActions = adminLogoutActions;
|
||||||
this.backChannelLogoutTokens = backChannelLogoutTokens;
|
this.backChannelLogoutTokens = backChannelLogoutTokens;
|
||||||
|
this.frontChannelLogoutTokens = frontChannelLogoutTokens;
|
||||||
this.adminPushNotBeforeActions = adminPushNotBeforeActions;
|
this.adminPushNotBeforeActions = adminPushNotBeforeActions;
|
||||||
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
this.adminTestAvailabilityAction = adminTestAvailabilityAction;
|
||||||
this.oidcClientData = oidcClientData;
|
this.oidcClientData = oidcClientData;
|
||||||
|
@ -101,6 +104,15 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
backChannelLogoutTokens.add(new JWSInput(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN)).readJsonContent(LogoutToken.class));
|
backChannelLogoutTokens.add(new JWSInput(request.getDecodedFormParameters().getFirst(OAuth2Constants.LOGOUT_TOKEN)).readJsonContent(LogoutToken.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/admin/frontchannelLogout")
|
||||||
|
public void frontchannelLogout(@QueryParam("sid") String sid, @QueryParam("iss") String issuer) {
|
||||||
|
LogoutToken token = new LogoutToken();
|
||||||
|
token.setSid(sid);
|
||||||
|
token.issuer(issuer);
|
||||||
|
frontChannelLogoutTokens.add(token);
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.TEXT_PLAIN_UTF_8)
|
@Consumes(MediaType.TEXT_PLAIN_UTF_8)
|
||||||
@Path("/admin/k_push_not_before")
|
@Path("/admin/k_push_not_before")
|
||||||
|
@ -129,6 +141,13 @@ public class TestApplicationResourceProvider implements RealmResourceProvider {
|
||||||
return backChannelLogoutTokens.poll(20, TimeUnit.SECONDS);
|
return backChannelLogoutTokens.poll(20, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/poll-frontchannel-logout")
|
||||||
|
public LogoutToken getFrontChannelLogoutAction() throws InterruptedException {
|
||||||
|
return frontChannelLogoutTokens.poll(20, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/poll-admin-not-before")
|
@Path("/poll-admin-not-before")
|
||||||
|
|
|
@ -47,6 +47,7 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
|
|
||||||
private BlockingQueue<LogoutAction> adminLogoutActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<LogoutAction> adminLogoutActions = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<LogoutToken> backChannelLogoutTokens = new LinkedBlockingDeque<>();
|
private BlockingQueue<LogoutToken> backChannelLogoutTokens = new LinkedBlockingDeque<>();
|
||||||
|
private BlockingQueue<LogoutToken> frontChannelLogoutTokens = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<PushNotBeforeAction> pushNotBeforeActions = new LinkedBlockingDeque<>();
|
||||||
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
private BlockingQueue<TestAvailabilityAction> testAvailabilityActions = new LinkedBlockingDeque<>();
|
||||||
|
|
||||||
|
@ -57,7 +58,7 @@ public class TestApplicationResourceProviderFactory implements RealmResourceProv
|
||||||
@Override
|
@Override
|
||||||
public RealmResourceProvider create(KeycloakSession session) {
|
public RealmResourceProvider create(KeycloakSession session) {
|
||||||
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
|
TestApplicationResourceProvider provider = new TestApplicationResourceProvider(session, adminLogoutActions,
|
||||||
backChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications);
|
backChannelLogoutTokens, frontChannelLogoutTokens, pushNotBeforeActions, testAvailabilityActions, oidcClientData, authenticationChannelRequests, cibaClientNotifications);
|
||||||
|
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
ResteasyProviderFactory.getInstance().injectProperties(provider);
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,11 @@ public interface TestApplicationResource {
|
||||||
@Path("/poll-backchannel-logout")
|
@Path("/poll-backchannel-logout")
|
||||||
LogoutToken getBackChannelLogoutToken();
|
LogoutToken getBackChannelLogoutToken();
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Path("/poll-frontchannel-logout")
|
||||||
|
LogoutToken getFrontChannelLogoutToken();
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Path("/poll-admin-not-before")
|
@Path("/poll-admin-not-before")
|
||||||
|
|
|
@ -55,9 +55,11 @@ import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AuthorizationResponseToken;
|
import org.keycloak.representations.AuthorizationResponseToken;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.RefreshToken;
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
@ -79,6 +81,7 @@ import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutorFa
|
||||||
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureClientUrisExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.SecureClientUrisExecutorFactory;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureLogoutExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
|
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory;
|
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory;
|
||||||
|
@ -119,6 +122,7 @@ import java.util.Optional;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
||||||
|
@ -144,6 +148,7 @@ import static org.keycloak.testsuite.util.ClientPoliciesUtil.createSecureSigning
|
||||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createTestRaiseExeptionConditionConfig;
|
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createTestRaiseExeptionConditionConfig;
|
||||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createFullScopeDisabledExecutorConfig;
|
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createFullScopeDisabledExecutorConfig;
|
||||||
|
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||||
|
@ -2579,6 +2584,88 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
||||||
assertEquals("Exception thrown intentionally", tokenResponse.getErrorDescription());
|
assertEquals("Exception thrown intentionally", tokenResponse.getErrorDescription());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureLogoutExecutor() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Logout Test")
|
||||||
|
.addExecutor(SecureLogoutExecutorFactory.PROVIDER_ID, null)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Logout Policy", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
String clientId = generateSuffixedName(CLIENT_NAME);
|
||||||
|
String clientSecret = "secret";
|
||||||
|
try {
|
||||||
|
createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setSecret(clientSecret);
|
||||||
|
clientRep.setStandardFlowEnabled(Boolean.TRUE);
|
||||||
|
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
|
||||||
|
clientRep.setPublicClient(Boolean.FALSE);
|
||||||
|
clientRep.setFrontchannelLogout(true);
|
||||||
|
});
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
assertEquals("Front-channel logout is not allowed for this client", cpe.getErrorDetail());
|
||||||
|
}
|
||||||
|
|
||||||
|
String cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setSecret(clientSecret);
|
||||||
|
clientRep.setStandardFlowEnabled(Boolean.TRUE);
|
||||||
|
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
|
||||||
|
clientRep.setPublicClient(Boolean.FALSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClientResource clientResource = adminClient.realm(REALM_NAME).clients().get(cid);
|
||||||
|
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||||
|
|
||||||
|
clientRep.setFrontchannelLogout(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
} catch (BadRequestException bre) {
|
||||||
|
assertEquals("Front-channel logout is not allowed for this client", bre.getResponse().readEntity(OAuth2ErrorRepresentation.class).getErrorDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientPolicyExecutorConfigurationRepresentation config = new ClientPolicyExecutorConfigurationRepresentation();
|
||||||
|
|
||||||
|
config.setConfigAsMap(SecureLogoutExecutorFactory.ALLOW_FRONT_CHANNEL_LOGOUT, Boolean.TRUE.booleanValue());
|
||||||
|
|
||||||
|
json = (new ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Logout Test")
|
||||||
|
.addExecutor(SecureLogoutExecutorFactory.PROVIDER_ID, config)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setFrontChannelLogoutUrl(oauth.getRedirectUri());
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
|
||||||
|
config.setConfigAsMap(SecureLogoutExecutorFactory.ALLOW_FRONT_CHANNEL_LOGOUT, Boolean.FALSE.toString());
|
||||||
|
|
||||||
|
json = (new ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Logout Test")
|
||||||
|
.addExecutor(SecureLogoutExecutorFactory.PROVIDER_ID, config)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
successfulLogin(clientId, clientSecret);
|
||||||
|
|
||||||
|
oauth.openLogout();
|
||||||
|
|
||||||
|
assertTrue(driver.getPageSource().contains("Front-channel logout is not allowed for this client"));
|
||||||
|
}
|
||||||
|
|
||||||
private void openVerificationPage(String verificationUri) {
|
private void openVerificationPage(String verificationUri) {
|
||||||
driver.navigate().to(verificationUri);
|
driver.navigate().to(verificationUri);
|
||||||
}
|
}
|
||||||
|
@ -2753,6 +2840,12 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void successfulLoginAndLogout(String clientId, String clientSecret) {
|
private void successfulLoginAndLogout(String clientId, String clientSecret) {
|
||||||
|
OAuthClient.AccessTokenResponse res = successfulLogin(clientId, clientSecret);
|
||||||
|
oauth.doLogout(res.getRefreshToken(), clientSecret);
|
||||||
|
events.expectLogout(res.getSessionState()).client(clientId).clearDetails().assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuthClient.AccessTokenResponse successfulLogin(String clientId, String clientSecret) {
|
||||||
oauth.clientId(clientId);
|
oauth.clientId(clientId);
|
||||||
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
|
oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);
|
||||||
|
|
||||||
|
@ -2764,8 +2857,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
||||||
assertEquals(200, res.getStatusCode());
|
assertEquals(200, res.getStatusCode());
|
||||||
events.expectCodeToToken(codeId, sessionId).client(clientId).assertEvent();
|
events.expectCodeToToken(codeId, sessionId).client(clientId).assertEvent();
|
||||||
|
|
||||||
oauth.doLogout(res.getRefreshToken(), clientSecret);
|
return res;
|
||||||
events.expectLogout(sessionId).client(clientId).clearDetails().assertEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void successfulLoginAndLogoutWithPKCE(String clientId, String clientSecret, String userName, String userPassword) throws Exception {
|
private void successfulLoginAndLogoutWithPKCE(String clientId, String clientSecret, String userName, String userPassword) throws Exception {
|
||||||
|
|
|
@ -89,6 +89,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
client.setClientName("RegistrationAccessTokenTest");
|
client.setClientName("RegistrationAccessTokenTest");
|
||||||
client.setClientUri("http://root");
|
client.setClientUri("http://root");
|
||||||
client.setRedirectUris(Collections.singletonList("http://redirect"));
|
client.setRedirectUris(Collections.singletonList("http://redirect"));
|
||||||
|
client.setFrontChannelLogoutUri("http://frontchannel");
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,6 +158,7 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
|
assertEquals(Arrays.asList(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN), response.getGrantTypes());
|
||||||
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
|
assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, response.getTokenEndpointAuthMethod());
|
||||||
Assert.assertNull(response.getUserinfoSignedResponseAlg());
|
Assert.assertNull(response.getUserinfoSignedResponseAlg());
|
||||||
|
assertEquals("http://frontchannel", response.getFrontChannelLogoutUri());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -22,10 +22,16 @@ import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.admin.client.resource.ClientResource;
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.admin.client.resource.ClientsResource;
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
@ -348,4 +354,64 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFrontChannelLogoutWithPostLogoutRedirectUri() 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");
|
||||||
|
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);
|
||||||
|
|
||||||
|
IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class);
|
||||||
|
|
||||||
|
Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer());
|
||||||
|
Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId());
|
||||||
|
} finally {
|
||||||
|
rep.setFrontchannelLogout(false);
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
|
||||||
|
clients.get(rep.getId()).update(rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFrontChannelLogout() throws Exception {
|
||||||
|
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
|
||||||
|
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
|
||||||
|
rep.setName("My Testing App");
|
||||||
|
rep.setFrontchannelLogout(true);
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, oauth.APP_ROOT + "/admin/frontchannelLogout");
|
||||||
|
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).build();
|
||||||
|
driver.navigate().to(logoutUrl);
|
||||||
|
LogoutToken logoutToken = testingClient.testApp().getFrontChannelLogoutToken();
|
||||||
|
Assert.assertNotNull(logoutToken);
|
||||||
|
IDToken idToken = new JWSInput(idTokenString).readJsonContent(IDToken.class);
|
||||||
|
Assert.assertEquals(logoutToken.getIssuer(), idToken.getIssuer());
|
||||||
|
Assert.assertEquals(logoutToken.getSid(), idToken.getSessionId());
|
||||||
|
assertTrue(driver.getTitle().equals("Logging out"));
|
||||||
|
assertTrue(driver.getPageSource().contains("You are logging out from following apps"));
|
||||||
|
assertTrue(driver.getPageSource().contains("My Testing App"));
|
||||||
|
} finally {
|
||||||
|
rep.setFrontchannelLogout(false);
|
||||||
|
rep.getAttributes().put(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI, "");
|
||||||
|
clients.get(rep.getId()).update(rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,6 +206,10 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
assertEquals(oauth.getParEndpointUrl(), oidcConfig.getPushedAuthorizationRequestEndpoint());
|
assertEquals(oauth.getParEndpointUrl(), oidcConfig.getPushedAuthorizationRequestEndpoint());
|
||||||
assertEquals(Boolean.FALSE, oidcConfig.getRequirePushedAuthorizationRequests());
|
assertEquals(Boolean.FALSE, oidcConfig.getRequirePushedAuthorizationRequests());
|
||||||
|
|
||||||
|
// frontchannel logout
|
||||||
|
assertTrue(oidcConfig.getFrontChannelLogoutSessionSupported());
|
||||||
|
assertTrue(oidcConfig.getFrontChannelLogoutSupported());
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -360,6 +360,9 @@ force-post-binding=Force POST Binding
|
||||||
force-post-binding.tooltip=Always use POST binding for responses.
|
force-post-binding.tooltip=Always use POST binding for responses.
|
||||||
front-channel-logout=Front Channel Logout
|
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.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.
|
||||||
|
|
||||||
force-name-id-format=Force Name ID Format
|
force-name-id-format=Force Name ID Format
|
||||||
force-name-id-format.tooltip=Ignore requested NameID subject format and use admin console configured one.
|
force-name-id-format.tooltip=Ignore requested NameID subject format and use admin console configured one.
|
||||||
name-id-format=Name ID Format
|
name-id-format=Name ID Format
|
||||||
|
|
|
@ -1777,6 +1777,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
||||||
} else {
|
} else {
|
||||||
$scope.clientEdit.attributes["request.uris"] = null;
|
$scope.clientEdit.attributes["request.uris"] = null;
|
||||||
}
|
}
|
||||||
|
if (!$scope.clientEdit.frontchannelLogout) {
|
||||||
|
$scope.clientEdit.attributes["frontchannel.logout.url"] = null;
|
||||||
|
}
|
||||||
delete $scope.clientEdit.requestUris;
|
delete $scope.clientEdit.requestUris;
|
||||||
|
|
||||||
if ($scope.samlArtifactBinding == true) {
|
if ($scope.samlArtifactBinding == true) {
|
||||||
|
|
|
@ -271,13 +271,20 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'force-post-binding.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'force-post-binding.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
<div class="form-group clearfix block" data-ng-show="protocol == 'saml' || protocol == 'openid-connect'">
|
||||||
<label class="col-md-2 control-label" for="frontchannelLogout">{{:: 'front-channel-logout' | translate}}</label>
|
<label class="col-md-2 control-label" for="frontchannelLogout">{{:: 'front-channel-logout' | translate}}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<input ng-model="clientEdit.frontchannelLogout" name="frontchannelLogout" id="frontchannelLogout" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
<input ng-model="clientEdit.frontchannelLogout" name="frontchannelLogout" id="frontchannelLogout" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'front-channel-logout.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'front-channel-logout.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" data-ng-show="protocol == 'openid-connect' && clientEdit.frontchannelLogout">
|
||||||
|
<label class="col-md-2 control-label" for="frontchannelLogoutUrl">{{:: 'front-channel-logout-url' | translate}}</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input class="form-control" type="text" name="frontchannelLogoutUrl" id="frontchannelLogoutUrl" data-ng-model="clientEdit.attributes['frontchannel.logout.url']">
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'front-channel-logout-url.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
<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>
|
<label class="col-md-2 control-label" for="samlForceNameIdFormat">{{:: 'force-name-id-format' | translate}}</label>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section = "header">
|
||||||
|
<script>
|
||||||
|
document.title = "${msg("frontchannel-logout.title")}";
|
||||||
|
</script>
|
||||||
|
${msg("frontchannel-logout.title")}
|
||||||
|
<#elseif section = "form">
|
||||||
|
<p>${msg("frontchannel-logout.message")}</p>
|
||||||
|
<ul>
|
||||||
|
<#list logout.clients as client>
|
||||||
|
<li>
|
||||||
|
${client.name}
|
||||||
|
<iframe src="${client.frontChannelLogoutUrl}" style="display:none;"></iframe>
|
||||||
|
</li>
|
||||||
|
</#list>
|
||||||
|
</ul>
|
||||||
|
<#if logout.logoutRedirectUri?has_content>
|
||||||
|
<script>
|
||||||
|
function readystatechange(event) {
|
||||||
|
if (document.readyState=='complete') {
|
||||||
|
window.location.replace('${logout.logoutRedirectUri}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('readystatechange', readystatechange);
|
||||||
|
</script>
|
||||||
|
<a id="continue" class="btn btn-primary" href="${logout.logoutRedirectUri}">${msg("doContinue")}</a>
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
|
@ -439,3 +439,6 @@ accountUnusable=Any subsequent use of the application will not be possible with
|
||||||
userDeletedSuccessfully=User deleted successfully
|
userDeletedSuccessfully=User deleted successfully
|
||||||
|
|
||||||
access-denied=Access denied
|
access-denied=Access denied
|
||||||
|
|
||||||
|
frontchannel-logout.title=Logging out
|
||||||
|
frontchannel-logout.message=You are logging out from following apps
|
||||||
|
|
Loading…
Reference in a new issue