saml signatures

This commit is contained in:
Bill Burke 2014-10-16 09:14:04 -04:00
parent 4e41041469
commit 4750b22b6d
12 changed files with 430 additions and 248 deletions

View file

@ -8,7 +8,6 @@ import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ProcessingException; import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.util.DocumentUtil; import org.picketlink.common.util.DocumentUtil;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator; import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory; import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder; import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
@ -43,7 +42,7 @@ import static org.picketlink.common.util.StringUtil.isNotNull;
* @author Anil.Saldhana@redhat.com * @author Anil.Saldhana@redhat.com
* @author bburke@redhat.com * @author bburke@redhat.com
*/ */
public class SAML2PostBindingResponseBuilder { public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilder<SALM2PostBindingLoginResponseBuilder> {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger(); protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
protected List<String> roles = new LinkedList<String>(); protected List<String> roles = new LinkedList<String>();
@ -51,188 +50,68 @@ public class SAML2PostBindingResponseBuilder {
protected boolean multiValuedRoles; protected boolean multiValuedRoles;
protected boolean disableAuthnStatement; protected boolean disableAuthnStatement;
protected String requestID; protected String requestID;
protected String responseIssuer;
protected String authMethod; protected String authMethod;
protected String relayState;
protected String destination;
protected String requestIssuer; protected String requestIssuer;
protected Map<String, Object> attributes = new HashMap<String, Object>(); protected Map<String, Object> attributes = new HashMap<String, Object>();
public SAML2PostBindingResponseBuilder attributes(Map<String, Object> attributes) { public SALM2PostBindingLoginResponseBuilder attributes(Map<String, Object> attributes) {
this.attributes = attributes; this.attributes = attributes;
return this; return this;
} }
public SAML2PostBindingResponseBuilder attribute(String name, Object value) { public SALM2PostBindingLoginResponseBuilder attribute(String name, Object value) {
this.attributes.put(name, value); this.attributes.put(name, value);
return this; return this;
} }
public SAML2PostBindingResponseBuilder requestID(String requestID) { public SALM2PostBindingLoginResponseBuilder requestID(String requestID) {
this.requestID =requestID; this.requestID =requestID;
return this; return this;
} }
public SAML2PostBindingResponseBuilder requestIssuer(String requestIssuer) { public SALM2PostBindingLoginResponseBuilder requestIssuer(String requestIssuer) {
this.requestIssuer =requestIssuer; this.requestIssuer =requestIssuer;
return this; return this;
} }
public SAML2PostBindingResponseBuilder responseIssuer(String issuer) { public SALM2PostBindingLoginResponseBuilder roles(List<String> roles) {
this.responseIssuer = issuer;
return this;
}
public SAML2PostBindingResponseBuilder roles(List<String> roles) {
this.roles = roles; this.roles = roles;
return this; return this;
} }
public SAML2PostBindingResponseBuilder roles(String... roles) { public SALM2PostBindingLoginResponseBuilder roles(String... roles) {
for (String role : roles) { for (String role : roles) {
this.roles.add(role); this.roles.add(role);
} }
return this; return this;
} }
public SAML2PostBindingResponseBuilder authMethod(String authMethod) { public SALM2PostBindingLoginResponseBuilder authMethod(String authMethod) {
this.authMethod = authMethod; this.authMethod = authMethod;
return this; return this;
} }
public SAML2PostBindingResponseBuilder userPrincipal(String userPrincipal) { public SALM2PostBindingLoginResponseBuilder userPrincipal(String userPrincipal) {
this.userPrincipal = userPrincipal; this.userPrincipal = userPrincipal;
return this; return this;
} }
public SAML2PostBindingResponseBuilder relayState(String relayState) { public SALM2PostBindingLoginResponseBuilder multiValuedRoles(boolean multiValuedRoles) {
this.relayState = relayState;
return this;
}
public SAML2PostBindingResponseBuilder destination(String destination) {
this.destination = destination;
return this;
}
public SAML2PostBindingResponseBuilder multiValuedRoles(boolean multiValuedRoles) {
this.multiValuedRoles = multiValuedRoles; this.multiValuedRoles = multiValuedRoles;
return this; return this;
} }
public SAML2PostBindingResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) { public SALM2PostBindingLoginResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) {
this.disableAuthnStatement = disableAuthnStatement; this.disableAuthnStatement = disableAuthnStatement;
return this; return this;
} }
public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException {
Document doc = getErrorResponse(status);
return buildResponse(doc);
}
public Document getErrorResponse(String status) {
Document samlResponse = null;
ResponseType responseType = null;
SAML2Response saml2Response = new SAML2Response();
// Create a response type
String id = IDGenerator.create("ID_");
IssuerInfoHolder issuerHolder = new IssuerInfoHolder(responseIssuer);
issuerHolder.setStatusCode(status);
IDPInfoHolder idp = new IDPInfoHolder();
idp.setNameIDFormatValue(null);
idp.setNameIDFormat(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get());
SPInfoHolder sp = new SPInfoHolder();
sp.setResponseDestinationURI(destination);
responseType = saml2Response.createResponseType(id);
responseType.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status));
responseType.setDestination(destination);
// Lets see how the response looks like
if (logger.isTraceEnabled()) {
StringWriter sw = new StringWriter();
try {
saml2Response.marshall(responseType, sw);
} catch (ProcessingException e) {
logger.trace(e);
}
logger.trace("SAML Response Document: " + sw.toString());
}
/*
if (supportSignature) {
try {
SAML2Signature ss = new SAML2Signature();
samlResponse = ss.sign(responseType, keyManager.getSigningKeyPair());
} catch (Exception e) {
logger.trace(e);
throw new RuntimeException(logger.signatureError(e));
}
} else
try {
samlResponse = saml2Response.convert(responseType);
} catch (Exception e) {
logger.trace(e);
}
*/
return samlResponse;
}
public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException { public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException {
Document responseDoc = getResponse(); Document responseDoc = getResponse();
return buildResponse(responseDoc); return buildResponse(responseDoc);
} }
protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8");
String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes));
if (destination == null) {
throw logger.nullValueError("Destination is null");
}
StringBuilder builder = new StringBuilder();
String key = GeneralConstants.SAML_RESPONSE_KEY;
builder.append("<HTML>");
builder.append("<HEAD>");
builder.append("<TITLE>HTTP Post Binding Response (Response)</TITLE>");
builder.append("</HEAD>");
builder.append("<BODY Onload=\"document.forms[0].submit()\">");
builder.append("<FORM METHOD=\"POST\" ACTION=\"" + destination + "\">");
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + key + "\"" + " VALUE=\"" + samlResponse + "\"/>");
if (isNotNull(relayState)) {
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"" + relayState + "\"/>");
}
builder.append("<NOSCRIPT>");
builder.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>");
builder.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />");
builder.append("</NOSCRIPT>");
builder.append("</FORM></BODY></HTML>");
String str = builder.toString();
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
}
public Document getResponse() throws ConfigurationException, ProcessingException { public Document getResponse() throws ConfigurationException, ProcessingException {
Document samlResponseDocument = null; Document samlResponseDocument = null;
@ -295,6 +174,10 @@ public class SAML2PostBindingResponseBuilder {
throw logger.samlAssertionMarshallError(e); throw logger.samlAssertionMarshallError(e);
} }
if (signed) {
signDocument(samlResponseDocument);
}
return samlResponseDocument; return samlResponseDocument;
} }
} }

View file

@ -0,0 +1,133 @@
package org.keycloak.protocol.saml;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.util.DocumentUtil;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import static org.picketlink.common.util.StringUtil.isNotNull;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
protected KeyPair signingKeyPair;
protected X509Certificate signingCertificate;
protected boolean signed;
protected String signatureDigestMethod;
protected String signatureMethod;
protected String relayState;
protected String destination;
protected String responseIssuer;
public T sign(KeyPair keyPair) {
this.signingKeyPair = keyPair;
this.signed = true;
return (T)this;
}
public T sign(PrivateKey privateKey, PublicKey publicKey) {
this.signingKeyPair = new KeyPair(publicKey, privateKey);
this.signed = true;
return (T)this;
}
public T sign(KeyPair keyPair, X509Certificate cert) {
this.signingKeyPair = keyPair;
this.signingCertificate = cert;
this.signed = true;
return (T)this;
}
public T signatureDigestMethod(String method) {
this.signatureDigestMethod = method;
return (T)this;
}
public T signatureMethod(String method) {
this.signatureMethod = method;
return (T)this;
}
public T sign(PrivateKey privateKey, PublicKey publicKey, X509Certificate cert) {
this.signingKeyPair = new KeyPair(publicKey, privateKey);
this.signingCertificate = cert;
this.signed = true;
return (T)this;
}
public T destination(String destination) {
this.destination = destination;
return (T)this;
}
public T responseIssuer(String issuer) {
this.responseIssuer = issuer;
return (T)this;
}
public T relayState(String relayState) {
this.relayState = relayState;
return (T)this;
}
protected void signDocument(Document samlDocument) throws ProcessingException {
SamlProtocolUtils.signDocument(samlDocument, signingKeyPair, signatureMethod, signatureDigestMethod, signingCertificate);
}
protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8");
String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes));
if (destination == null) {
throw SALM2PostBindingLoginResponseBuilder.logger.nullValueError("Destination is null");
}
StringBuilder builder = new StringBuilder();
String key = GeneralConstants.SAML_RESPONSE_KEY;
builder.append("<HTML>");
builder.append("<HEAD>");
builder.append("<TITLE>HTTP Post Binding Response (Response)</TITLE>");
builder.append("</HEAD>");
builder.append("<BODY Onload=\"document.forms[0].submit()\">");
builder.append("<FORM METHOD=\"POST\" ACTION=\"" + destination + "\">");
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"" + key + "\"" + " VALUE=\"" + samlResponse + "\"/>");
if (isNotNull(relayState)) {
builder.append("<INPUT TYPE=\"HIDDEN\" NAME=\"RelayState\" " + "VALUE=\"" + relayState + "\"/>");
}
builder.append("<NOSCRIPT>");
builder.append("<P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>");
builder.append("<INPUT TYPE=\"SUBMIT\" VALUE=\"CONTINUE\" />");
builder.append("</NOSCRIPT>");
builder.append("</FORM></BODY></HTML>");
String str = builder.toString();
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
}
}

View file

@ -0,0 +1,57 @@
package org.keycloak.protocol.saml;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.StringWriter;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2PostBindingErrorResponseBuilder extends SAML2PostBindingBuilder<SAML2PostBindingErrorResponseBuilder> {
public Document getErrorResponse(String status) throws ProcessingException {
Document samlResponse = null;
ResponseType responseType = null;
SAML2Response saml2Response = new SAML2Response();
// Create a response type
String id = IDGenerator.create("ID_");
IssuerInfoHolder issuerHolder = new IssuerInfoHolder(responseIssuer);
issuerHolder.setStatusCode(status);
IDPInfoHolder idp = new IDPInfoHolder();
idp.setNameIDFormatValue(null);
idp.setNameIDFormat(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get());
SPInfoHolder sp = new SPInfoHolder();
sp.setResponseDestinationURI(destination);
responseType = saml2Response.createResponseType(id);
responseType.setStatus(JBossSAMLAuthnResponseFactory.createStatusTypeForResponder(status));
responseType.setDestination(destination);
if (signed) {
signDocument(samlResponse);
}
return samlResponse;
}
public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException {
Document doc = getErrorResponse(status);
return buildResponse(doc);
}}

View file

@ -0,0 +1,69 @@
package org.keycloak.protocol.saml;
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.identity.federation.api.saml.v2.request.SAML2Request;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
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.saml.v2.protocol.ResponseType;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.security.KeyPair;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2PostBindingLogoutResponseBuilder extends SAML2PostBindingBuilder<SAML2PostBindingLogoutResponseBuilder> {
protected String userPrincipal;
public SAML2PostBindingLogoutResponseBuilder userPrincipal(String userPrincipal) {
this.userPrincipal = userPrincipal;
return this;
}
public String buildRequestString() {
try {
Document logoutRequestDocument = new SAML2Request().convert(createLogoutRequest());
if (signed) {
signDocument(logoutRequestDocument);
}
byte[] responseBytes = DocumentUtil.getDocumentAsString(logoutRequestDocument).getBytes("UTF-8");
return PostBindingUtil.base64Encode(new String(responseBytes));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private LogoutRequestType createLogoutRequest() throws ConfigurationException {
LogoutRequestType lort = new SAML2Request().createLogoutRequest(responseIssuer);
NameIDType nameID = new NameIDType();
nameID.setValue(userPrincipal);
//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(destination));
return lort;
}
}

View file

@ -43,6 +43,5 @@ public class SAMLRequestParser {
logger.samlBase64DecodingError(e); logger.samlBase64DecodingError(e);
} }
return null; return null;
} }
} }

View file

@ -20,34 +20,19 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Flows;
import org.picketlink.common.constants.GeneralConstants; import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.constants.JBossSAMLURIConstants; 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.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.handlers.saml2.SAML2LogOutHandler;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; 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> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SamlLogin implements LoginProtocol { public class SalmProtocol implements LoginProtocol {
protected static final Logger logger = Logger.getLogger(SamlLogin.class); protected static final Logger logger = Logger.getLogger(SalmProtocol.class);
public static final String LOGIN_PROTOCOL = "saml"; public static final String LOGIN_PROTOCOL = "saml";
public static final String SAML_BINDING = "saml_binding"; public static final String SAML_BINDING = "saml_binding";
public static final String SAML_POST_BINDING = "post"; public static final String SAML_POST_BINDING = "post";
@ -61,19 +46,19 @@ public class SamlLogin implements LoginProtocol {
@Override @Override
public SamlLogin setSession(KeycloakSession session) { public SalmProtocol setSession(KeycloakSession session) {
this.session = session; this.session = session;
return this; return this;
} }
@Override @Override
public SamlLogin setRealm(RealmModel realm) { public SalmProtocol setRealm(RealmModel realm) {
this.realm = realm; this.realm = realm;
return this; return this;
} }
@Override @Override
public SamlLogin setUriInfo(UriInfo uriInfo) { public SalmProtocol setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo; this.uriInfo = uriInfo;
return this; return this;
} }
@ -93,14 +78,10 @@ public class SamlLogin implements LoginProtocol {
} }
protected Response getErrorResponse(ClientSessionModel clientSession, String status) { protected Response getErrorResponse(ClientSessionModel clientSession, String status) {
String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE); SAML2PostBindingErrorResponseBuilder builder = new SAML2PostBindingErrorResponseBuilder()
String redirectUri = clientSession.getRedirectUri(); .relayState(clientSession.getNote(GeneralConstants.RELAY_STATE))
SAML2PostBindingResponseBuilder builder = new SAML2PostBindingResponseBuilder(); .destination(clientSession.getRedirectUri())
String responseIssuer = getResponseIssuer(realm); .responseIssuer(getResponseIssuer(realm));
builder .relayState(relayState)
.destination(redirectUri)
.responseIssuer(responseIssuer)
.requestIssuer(clientSession.getClient().getClientId());
try { try {
return builder.buildErrorResponse(status); return builder.buildErrorResponse(status);
} catch (Exception e) { } catch (Exception e) {
@ -111,7 +92,7 @@ public class SamlLogin implements LoginProtocol {
@Override @Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
ClientSessionModel clientSession = accessCode.getClientSession(); ClientSessionModel clientSession = accessCode.getClientSession();
if (SamlLogin.SAML_POST_BINDING.equals(clientSession.getNote(SamlLogin.SAML_BINDING))) { if (SalmProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SalmProtocol.SAML_BINDING))) {
return postBinding(userSession, clientSession); return postBinding(userSession, clientSession);
} }
throw new RuntimeException("still need to implement redirect binding"); throw new RuntimeException("still need to implement redirect binding");
@ -123,7 +104,7 @@ public class SamlLogin implements LoginProtocol {
String redirectUri = clientSession.getRedirectUri(); String redirectUri = clientSession.getRedirectUri();
String responseIssuer = getResponseIssuer(realm); String responseIssuer = getResponseIssuer(realm);
SAML2PostBindingResponseBuilder builder = new SAML2PostBindingResponseBuilder(); SALM2PostBindingLoginResponseBuilder builder = new SALM2PostBindingLoginResponseBuilder();
builder.requestID(requestID) builder.requestID(requestID)
.relayState(relayState) .relayState(relayState)
.destination(redirectUri) .destination(redirectUri)
@ -140,7 +121,10 @@ public class SamlLogin implements LoginProtocol {
builder.roles(roleModel.getName()); builder.roles(roleModel.getName());
} }
} }
ClientModel client = clientSession.getClient();
if (requiresRealmSignature(client)) {
builder.sign(realm.getPrivateKey(), realm.getPublicKey());
}
try { try {
return builder.buildLoginResponse(); return builder.buildLoginResponse();
} catch (Exception e) { } catch (Exception e) {
@ -149,7 +133,11 @@ public class SamlLogin implements LoginProtocol {
} }
} }
public void initClaims(SAML2PostBindingResponseBuilder builder, ClientModel model, UserModel user) { private boolean requiresRealmSignature(ClientModel client) {
return "true".equals(client.getAttribute("samlServerSignature"));
}
public void initClaims(SALM2PostBindingLoginResponseBuilder builder, ClientModel model, UserModel user) {
if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) { if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) {
builder.attribute(X500SAMLProfileConstants.EMAIL_ADDRESS.getFriendlyName(), user.getEmail()); builder.attribute(X500SAMLProfileConstants.EMAIL_ADDRESS.getFriendlyName(), user.getEmail());
} }
@ -172,15 +160,18 @@ public class SamlLogin implements LoginProtocol {
ApplicationModel app = (ApplicationModel)client; ApplicationModel app = (ApplicationModel)client;
if (app.getManagementUrl() == null) return; if (app.getManagementUrl() == null) return;
SAML2PostBindingLogoutResponseBuilder logoutBuilder = new SAML2PostBindingLogoutResponseBuilder()
.userPrincipal(userSession.getUser().getUsername())
.destination(client.getClientId());
if (requiresRealmSignature(client)) {
logoutBuilder.sign(realm.getPrivateKey(), realm.getPublicKey());
}
String logoutRequestString = null; String logoutRequestString = null;
try { try {
LogoutRequestType logoutRequest = createLogoutRequest(userSession.getUser(), client); logoutRequestString = logoutBuilder.buildRequestString();
Document logoutRequestDocument = new SAML2Request().convert(logoutRequest);
byte[] responseBytes = DocumentUtil.getDocumentAsString(logoutRequestDocument).getBytes("UTF-8");
logoutRequestString = PostBindingUtil.base64Encode(new String(responseBytes));
} catch (Exception e) { } catch (Exception e) {
logger.warn("failed to send saml logout", e); logger.warn("failed to send saml logout", e);
return;
} }
@ -219,24 +210,6 @@ public class SamlLogin implements LoginProtocol {
} }
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 @Override
public void close() { public void close() {

View file

@ -13,7 +13,7 @@ import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SamlLoginFactory implements LoginProtocolFactory { public class SamlProtocolFactory implements LoginProtocolFactory {
@Override @Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) { public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
@ -22,7 +22,7 @@ public class SamlLoginFactory implements LoginProtocolFactory {
@Override @Override
public LoginProtocol create(KeycloakSession session) { public LoginProtocol create(KeycloakSession session) {
return new SamlLogin().setSession(session); return new SalmProtocol().setSession(session);
} }
@Override @Override

View file

@ -0,0 +1,66 @@
package org.keycloak.protocol.saml;
import org.keycloak.VerificationException;
import org.keycloak.models.ClientModel;
import org.keycloak.util.PemUtils;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.security.KeyPair;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlProtocolUtils {
public static void verifyPostBindingSignature(ClientModel client, Document document) throws VerificationException {
if (!"true".equals(client.getAttribute("samlClientSignature"))) {
return;
}
SAML2Signature saml2Signature = new SAML2Signature();
String publicKeyPem = client.getAttribute(ClientModel.PUBLIC_KEY);
if (publicKeyPem == null) throw new VerificationException("Client does not have a public key.");
PublicKey publicKey = null;
try {
publicKey = PemUtils.decodePublicKey(publicKeyPem);
} catch (Exception e) {
throw new VerificationException("Could not decode public key", e);
}
try {
if (!saml2Signature.validate(document, publicKey)) {
throw new VerificationException("Invalid signature on document");
}
} catch (ProcessingException e) {
throw new VerificationException("Error validating signature", e);
}
}
public static void signDocument(Document samlDocument, KeyPair signingKeyPair, String signatureMethod, String signatureDigestMethod, X509Certificate signingCertificate) throws ProcessingException {
SAML2Signature samlSignature = new SAML2Signature();
if (signatureMethod != null) {
samlSignature.setSignatureMethod(signatureMethod);
}
if (signatureDigestMethod != null) {
samlSignature.setDigestMethod(signatureDigestMethod);
}
Node nextSibling = samlSignature.getNextSiblingOfIssuer(samlDocument);
samlSignature.setNextSibling(nextSibling);
if (signingCertificate != null) {
samlSignature.setX509Certificate(signingCertificate);
}
samlSignature.signSAMLDocument(samlDocument, signingKeyPair);
}
}

View file

@ -1,13 +1,11 @@
package org.keycloak.protocol.saml; package org.keycloak.protocol.saml;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants; import org.keycloak.VerificationException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -27,15 +25,14 @@ 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.SAML2Object;
import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType; import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType;
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType; import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
import org.picketlink.identity.federation.saml.v2.protocol.RequestAbstractType;
import org.w3c.dom.Document;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam; import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -45,8 +42,6 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.net.URI; import java.net.URI;
import java.util.HashMap;
import java.util.Map;
/** /**
* Resource class for the oauth/openid connect token service * Resource class for the oauth/openid connect token service
@ -137,24 +132,7 @@ public class SamlService {
SAML2Object samlObject = documentHolder.getSamlObject(); SAML2Object samlObject = documentHolder.getSamlObject();
if (samlObject instanceof AuthnRequestType) { RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject;
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");
}
}
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType) {
String issuer = requestAbstractType.getIssuer().getValue(); String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.findClient(issuer); ClientModel client = realm.findClient(issuer);
@ -176,6 +154,32 @@ public class SamlService {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login");
} }
try {
SamlProtocolUtils.verifyPostBindingSignature(client, documentHolder.getSamlDocument());
} catch (VerificationException e) {
logger.error("request validation failed", e);
event.error(Errors.INVALID_CLIENT);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid requester.");
}
if (samlObject instanceof AuthnRequestType) {
event.event(EventType.LOGIN);
// Get the SAML Request Message
AuthnRequestType authn = (AuthnRequestType) samlObject;
return loginRequest(relayState, authn, client);
} else if (samlObject instanceof LogoutRequestType) {
event.event(EventType.LOGOUT);
LogoutRequestType logout = (LogoutRequestType) samlObject;
return logoutRequest(logout, client);
} else {
event.event(EventType.LOGIN_ERROR);
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
}
}
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
@ -186,10 +190,10 @@ public class SamlService {
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlLogin.LOGIN_PROTOCOL); clientSession.setAuthMethod(SalmProtocol.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect); clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
clientSession.setNote(SamlLogin.SAML_BINDING, SamlLogin.SAML_POST_BINDING); clientSession.setNote(SalmProtocol.SAML_BINDING, SalmProtocol.SAML_POST_BINDING);
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
clientSession.setNote("REQUEST_ID", requestAbstractType.getID()); clientSession.setNote("REQUEST_ID", requestAbstractType.getID());
@ -212,28 +216,7 @@ public class SamlService {
return forms.createLogin(); return forms.createLogin();
} }
protected Response logoutRequest(String relayState, LogoutRequestType requestAbstractType) { protected Response logoutRequest(LogoutRequestType requestAbstractType, ClientModel client) {
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.findClient(issuer);
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. // 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); AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
if (authResult != null) { if (authResult != null) {

View file

@ -1 +1 @@
org.keycloak.protocol.saml.SamlLoginFactory org.keycloak.protocol.saml.SamlProtocolFactory

View file

@ -157,11 +157,11 @@ public class AccountTest {
}); });
} }
// @Test @Test
// @Ignore @Ignore
// public void runit() throws Exception { public void runit() throws Exception {
// Thread.sleep(10000000); Thread.sleep(10000000);
// } }
@Test @Test
public void returnToAppFromQueryParam() { public void returnToAppFromQueryParam() {

View file

@ -32,11 +32,30 @@
"name": "http://localhost:8080/sales-post/", "name": "http://localhost:8080/sales-post/",
"enabled": true, "enabled": true,
"fullScopeAllowed": true, "fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8080/sales-post", "baseUrl": "http://localhost:8080/sales-post",
"adminUrl": "http://localhost:8080/sales-post", "adminUrl": "http://localhost:8080/sales-post",
"redirectUris": [ "redirectUris": [
"http://localhost:8080/sales-post/*" "http://localhost:8080/sales-post/*"
] ]
},
{
"name": "http://localhost:8080/sales-post-sig/",
"enabled": true,
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8080/sales-post-sig",
"adminUrl": "http://localhost:8080/sales-post-sig",
"redirectUris": [
"http://localhost:8080/sales-post-sig/*"
],
"attributes": {
"samlServerSignature": "true",
"samlClientSignature": "true",
"privateKey": "MIICWwIBAAKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQABAoGADaTtoG/+foOZUiLjRWKL/OmyavK9vjgyFtThNkZY4qHOh0h3og0RdSbgIxAsIpEa1FUwU2W5yvI6mNeJ3ibFgCgcxqPk6GkAC7DWfQfdQ8cS+dCuaFTs8ObIQEvU50YzeNPiiFxRA+MnauCUXaKm/PnDfjd4tPgru7XZvlGh0wECQQDsBbN2cKkBKpr/b5oJiBcBaSZtWiMNuYBDn9x8uORj+Gy/49BUIMHF2EWyxOWz6ocP5YiynNRkPe21Zus7PEr1AkEA5yWQOkxUTIg43s4pxNSeHtL+Ebqcg54lY2xOQK0yufxUVZI8ODctAKmVBMiCKpU3mZQquOaQicuGtocpgxlScQI/YM31zZ5nsxLGf/5GL6KhzPJT0IYn2nk7IoFu7bjn9BjwgcPurpLA52TNMYWQsTqAKwT6DEhG1NaRqNWNpb4VAkBehObAYBwMm5udyHIeEc+CzUalm0iLLa0eRdiN7AUVNpCJ2V2Uo0NcxPux1AgeP5xXydXafDXYkwhINWcNO9qRAkEA58ckAC5loUGwU5dLaugsGH/a2Q8Ac8bmPglwfCstYDpl8Gp/eimb1eKyvDEELOhyImAv4/uZV9wN85V0xZXWsw==",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVG8a7xGN6ZIkDbeecySygcDfsypjUMNPE4QJjis8B316CvsZQ0hcTTLUyiRpHlHZys2k3xEhHBHymFC1AONcvzZzpb40tAhLHO1qtAnut00khjAdjR3muLVdGkM/zMC7G5s9iIwBVhwOQhy+VsGnCH91EzkjZ4SVEr55KJoyQJQIDAQAB",
"X509Certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw=="
}
} }
], ],
"roles" : { "roles" : {