saml backchannel logout
This commit is contained in:
1 parent
c609ae3496
commit
6d5ab0f66b
12 files changed
+615
-512
No files matched your search
+2
-2
@@ -146,7 +146,7 @@ public class SAML2PostBindingResponseBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Response error(String status) throws ConfigurationException, ProcessingException, IOException {
|
||||
public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException {
|
||||
Document doc = getErrorResponse(status);
|
||||
return buildResponse(doc);
|
||||
|
||||
@@ -207,7 +207,7 @@ public class SAML2PostBindingResponseBuilder {
|
||||
return samlResponse;
|
||||
}
|
||||
|
||||
public Response build() throws ConfigurationException, ProcessingException, IOException {
|
||||
public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException {
|
||||
Document responseDoc = getResponse();
|
||||
return buildResponse(responseDoc);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.keycloak.protocol.saml;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.jboss.resteasy.client.ClientRequest;
|
||||
import org.jboss.resteasy.client.ClientResponse;
|
||||
import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor;
|
||||
import org.keycloak.models.ApplicationModel;
|
||||
import org.keycloak.models.ClaimMask;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
@@ -13,17 +15,31 @@ import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.services.resources.flows.Flows;
|
||||
import org.picketlink.common.constants.GeneralConstants;
|
||||
import org.picketlink.common.constants.JBossSAMLURIConstants;
|
||||
import org.picketlink.common.exceptions.ConfigurationException;
|
||||
import org.picketlink.common.exceptions.ParsingException;
|
||||
import org.picketlink.common.exceptions.ProcessingException;
|
||||
import org.picketlink.common.util.StringUtil;
|
||||
import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request;
|
||||
import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
|
||||
import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil;
|
||||
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
|
||||
import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS;
|
||||
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
|
||||
import org.picketlink.identity.federation.web.handlers.saml2.SAML2LogOutHandler;
|
||||
import org.picketlink.identity.federation.web.util.PostBindingUtil;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
@@ -39,11 +55,8 @@ public class SamlLogin implements LoginProtocol {
|
||||
|
||||
protected RealmModel realm;
|
||||
|
||||
protected HttpRequest request;
|
||||
|
||||
protected UriInfo uriInfo;
|
||||
|
||||
protected ClientConnection clientConnection;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -58,24 +71,12 @@ public class SamlLogin implements LoginProtocol {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlLogin setRequest(HttpRequest request) {
|
||||
this.request = request;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlLogin setUriInfo(UriInfo uriInfo) {
|
||||
this.uriInfo = uriInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SamlLogin setClientConnection(ClientConnection clientConnection) {
|
||||
this.clientConnection = clientConnection;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancelLogin(ClientSessionModel clientSession) {
|
||||
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
|
||||
@@ -100,7 +101,7 @@ public class SamlLogin implements LoginProtocol {
|
||||
.responseIssuer(responseIssuer)
|
||||
.requestIssuer(clientSession.getClient().getClientId());
|
||||
try {
|
||||
return builder.error(status);
|
||||
return builder.buildErrorResponse(status);
|
||||
} catch (Exception e) {
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response");
|
||||
}
|
||||
@@ -140,7 +141,7 @@ public class SamlLogin implements LoginProtocol {
|
||||
}
|
||||
|
||||
try {
|
||||
return builder.build();
|
||||
return builder.buildLoginResponse();
|
||||
} catch (Exception e) {
|
||||
logger.error("failed", e);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response");
|
||||
@@ -163,6 +164,66 @@ public class SamlLogin implements LoginProtocol {
|
||||
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
|
||||
ClientModel client = clientSession.getClient();
|
||||
if (!(client instanceof ApplicationModel)) return;
|
||||
ApplicationModel app = (ApplicationModel)client;
|
||||
if (app.getManagementUrl() == null) return;
|
||||
|
||||
String logoutRequestString = null;
|
||||
try {
|
||||
LogoutRequestType logoutRequest = createLogoutRequest(userSession.getUser(), client);
|
||||
Document logoutRequestDocument = new SAML2Request().convert(logoutRequest);
|
||||
|
||||
byte[] responseBytes = DocumentUtil.getDocumentAsString(logoutRequestDocument).getBytes("UTF-8");
|
||||
logoutRequestString = PostBindingUtil.base64Encode(new String(responseBytes));
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to send saml logout", e);
|
||||
}
|
||||
|
||||
|
||||
String adminUrl = ResourceAdminManager.getManagementUrl(uriInfo.getRequestUri(), app);
|
||||
|
||||
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
|
||||
|
||||
|
||||
try {
|
||||
ClientRequest request = executor.createRequest(adminUrl);
|
||||
request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString);
|
||||
request.formParameter(SAML2LogOutHandler.BACK_CHANNEL_LOGOUT, SAML2LogOutHandler.BACK_CHANNEL_LOGOUT);
|
||||
ClientResponse response = null;
|
||||
try {
|
||||
response = request.post();
|
||||
} catch (Exception e) {
|
||||
logger.warn("failed to send saml logout", e);
|
||||
}
|
||||
response.releaseConnection();
|
||||
|
||||
} finally {
|
||||
executor.getHttpClient().getConnectionManager().shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private LogoutRequestType createLogoutRequest(UserModel user, ClientModel client) throws ConfigurationException, ProcessingException {
|
||||
LogoutRequestType lort = new SAML2Request().createLogoutRequest(getResponseIssuer(realm));
|
||||
|
||||
NameIDType nameID = new NameIDType();
|
||||
nameID.setValue(user.getUsername());
|
||||
//Deal with NameID Format
|
||||
String nameIDFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get();
|
||||
nameID.setFormat(URI.create(nameIDFormat));
|
||||
lort.setNameID(nameID);
|
||||
|
||||
long assertionValidity = PicketLinkCoreSTS.instance().getConfiguration().getIssuedTokenTimeout();
|
||||
|
||||
lort.setNotOnOrAfter(XMLTimeUtil.add(lort.getIssueInstant(), assertionValidity));
|
||||
lort.setDestination(URI.create(client.getClientId()));
|
||||
return lort;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.picketlink.common.constants.GeneralConstants;
|
||||
import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder;
|
||||
import org.picketlink.identity.federation.saml.v2.SAML2Object;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType;
|
||||
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.FormParam;
|
||||
@@ -94,38 +95,66 @@ public class SamlService {
|
||||
@Path("POST")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response loginPage(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
event.event(EventType.LOGIN);
|
||||
public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
|
||||
@FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
|
||||
@FormParam(GeneralConstants.RELAY_STATE) String relayState) {
|
||||
if (!checkSsl()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required");
|
||||
}
|
||||
if (!realm.isEnabled()) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled");
|
||||
}
|
||||
|
||||
if (samlRequest == null) {
|
||||
if (samlRequest == null && samlResponse == null) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
|
||||
}
|
||||
|
||||
if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
|
||||
else return handleSamlResponse(samlResponse, relayState);
|
||||
}
|
||||
|
||||
protected Response handleSamlResponse(String samleResponse, String relayState) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
}
|
||||
|
||||
|
||||
protected Response handleSamlRequest(String samlRequest, String relayState) {
|
||||
SAMLDocumentHolder documentHolder = SAMLRequestParser.parsePostBinding(samlRequest);
|
||||
if (documentHolder == null) {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
}
|
||||
|
||||
SAML2Object samlObject = documentHolder.getSamlObject();
|
||||
if (!(samlObject instanceof AuthnRequestType)) {
|
||||
|
||||
if (samlObject instanceof AuthnRequestType) {
|
||||
event.event(EventType.LOGIN);
|
||||
// Get the SAML Request Message
|
||||
AuthnRequestType requestAbstractType = (AuthnRequestType) samlObject;
|
||||
return loginRequest(relayState, requestAbstractType);
|
||||
} else if (samlObject instanceof LogoutRequestType) {
|
||||
event.event(EventType.LOGOUT);
|
||||
LogoutRequestType requestAbstractType = (LogoutRequestType) samlObject;
|
||||
return logoutRequest(relayState, requestAbstractType);
|
||||
|
||||
} else {
|
||||
event.event(EventType.LOGIN_ERROR);
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
|
||||
}
|
||||
}
|
||||
|
||||
// Get the SAML Request Message
|
||||
AuthnRequestType requestAbstractType = (AuthnRequestType) samlObject;
|
||||
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType) {
|
||||
String issuer = requestAbstractType.getIssuer().getValue();
|
||||
ClientModel client = realm.findClient(issuer);
|
||||
|
||||
@@ -189,29 +218,42 @@ public class SamlService {
|
||||
return forms.createLogin();
|
||||
}
|
||||
|
||||
protected Response logoutRequest(String relayState, LogoutRequestType requestAbstractType) {
|
||||
String issuer = requestAbstractType.getIssuer().getValue();
|
||||
ClientModel client = realm.findClient(issuer);
|
||||
|
||||
/**
|
||||
* Logout user session. User must be logged in via a session cookie.
|
||||
*
|
||||
* @param redirectUri
|
||||
* @return
|
||||
*/
|
||||
@Path("logout")
|
||||
@GET
|
||||
@NoCache
|
||||
public Response logout(final @QueryParam("shit") String redirectUri) {
|
||||
event.event(EventType.LOGOUT);
|
||||
if (redirectUri != null) {
|
||||
event.detail(Details.REDIRECT_URI, redirectUri);
|
||||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester.");
|
||||
}
|
||||
|
||||
if (!client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled.");
|
||||
}
|
||||
if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login");
|
||||
}
|
||||
if (client.isDirectGrantsOnly()) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login");
|
||||
}
|
||||
|
||||
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
|
||||
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
|
||||
if (authResult != null) {
|
||||
logout(authResult.getSession());
|
||||
}
|
||||
|
||||
String redirectUri = null;
|
||||
|
||||
if (client instanceof ApplicationModel) {
|
||||
redirectUri = ((ApplicationModel)client).getBaseUrl();
|
||||
}
|
||||
|
||||
if (redirectUri != null) {
|
||||
String validatedRedirect = OpenIDConnectService.verifyRealmRedirectUri(uriInfo, redirectUri, realm);
|
||||
String validatedRedirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);;
|
||||
if (validatedRedirect == null) {
|
||||
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri.");
|
||||
}
|
||||
@@ -219,6 +261,7 @@ public class SamlService {
|
||||
} else {
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void logout(UserSessionModel userSession) {
|
||||
@@ -233,14 +276,4 @@ public class SamlService {
|
||||
return !realm.getSslRequired().isRequired(clientConnection);
|
||||
}
|
||||
}
|
||||
|
||||
private Response createError(String error, String errorDescription, Response.Status status) {
|
||||
Map<String, String> e = new HashMap<String, String>();
|
||||
e.put(OAuth2Constants.ERROR, error);
|
||||
if (errorDescription != null) {
|
||||
e.put(OAuth2Constants.ERROR_DESCRIPTION, errorDescription);
|
||||
}
|
||||
return Response.status(status).entity(e).type("application/json").build();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user