Merge pull request #741 from patriot1burke/master

saml basic
This commit is contained in:
Bill Burke 2014-10-04 22:15:56 -04:00
commit e01424b815
14 changed files with 1105 additions and 6 deletions

View file

@ -138,6 +138,17 @@
<version>${project.version}</version>
</dependency>
<!-- saml -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-protocol</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-federation</artifactId>
</dependency>
<!-- mongo -->
<dependency>
<groupId>org.keycloak</groupId>

View file

@ -18,7 +18,7 @@
<resteasy.version>2.3.7.Final</resteasy.version>
<resteasy.version.latest>3.0.9.Final</resteasy.version.latest>
<undertow.version>1.0.15.Final</undertow.version>
<picketlink.version>2.7.0.Beta1</picketlink.version>
<picketlink.version>2.7.0.CR1-20140924</picketlink.version>
<picketbox.ldap.version>1.0.2.Final</picketbox.ldap.version>
<mongo.driver.version>2.11.3</mongo.driver.version>
<jboss.logging.version>3.1.4.GA</jboss.logging.version>
@ -107,6 +107,7 @@
<module>picketlink</module>
<module>federation</module>
<module>services</module>
<module>saml</module>
<module>social</module>
<module>forms</module>
<module>examples</module>
@ -229,6 +230,11 @@
<artifactId>picketlink-idm-impl</artifactId>
<version>${picketlink.version}</version>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-federation</artifactId>
<version>${picketlink.version}</version>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-idm-simple-schema</artifactId>

20
saml/pom.xml Executable file
View file

@ -0,0 +1,20 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Keycloak SAML Integration</name>
<description/>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-saml-pom</artifactId>
<packaging>pom</packaging>
<modules>
<module>saml-core</module>
<module>saml-protocol</module>
</modules>
</project>

55
saml/saml-core/pom.xml Executable file
View file

@ -0,0 +1,55 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-saml-core</artifactId>
<name>Keycloak SAML Core</name>
<description/>
<properties>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
</properties>
<dependencies>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-federation</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

128
saml/saml-protocol/pom.xml Executable file
View file

@ -0,0 +1,128 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-saml-protocol</artifactId>
<name>Keycloak SAML Protocol</name>
<description/>
<properties>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-forms-common-freemarker</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-events-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-account-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-email-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-login-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.picketlink</groupId>
<artifactId>picketlink-federation</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,320 @@
package org.keycloak.protocol.saml;
/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
import org.picketlink.common.PicketLinkLogger;
import org.picketlink.common.PicketLinkLoggerFactory;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.util.DocumentUtil;
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.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.StatementUtil;
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType;
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.CacheControl;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.picketlink.common.util.StringUtil.isNotNull;
/**
* <p> Handles for dealing with SAML2 Authentication </p>
* <p/>
* Configuration Options:
*
* @author Anil.Saldhana@redhat.com
*/
public class SAML2PostBindingResponseBuilder {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
protected List<String> roles = new LinkedList<String>();
protected String userPrincipal;
protected boolean multiValuedRoles;
protected boolean disableAuthnStatement;
protected String requestID;
protected String responseIssuer;
protected String authMethod;
protected String relayState;
protected String destination;
protected String requestIssuer;
protected Map<String, Object> attributes = new HashMap<String, Object>();
public SAML2PostBindingResponseBuilder attributes(Map<String, Object> attributes) {
this.attributes = attributes;
return this;
}
public SAML2PostBindingResponseBuilder attribute(String name, Object value) {
this.attributes.put(name, value);
return this;
}
public SAML2PostBindingResponseBuilder requestID(String requestID) {
this.requestID =requestID;
return this;
}
public SAML2PostBindingResponseBuilder requestIssuer(String requestIssuer) {
this.requestIssuer =requestIssuer;
return this;
}
public SAML2PostBindingResponseBuilder responseIssuer(String issuer) {
this.responseIssuer = issuer;
return this;
}
public SAML2PostBindingResponseBuilder roles(List<String> roles) {
this.roles = roles;
return this;
}
public SAML2PostBindingResponseBuilder roles(String... roles) {
for (String role : roles) {
this.roles.add(role);
}
return this;
}
public SAML2PostBindingResponseBuilder authMethod(String authMethod) {
this.authMethod = authMethod;
return this;
}
public SAML2PostBindingResponseBuilder userPrincipal(String userPrincipal) {
this.userPrincipal = userPrincipal;
return this;
}
public SAML2PostBindingResponseBuilder relayState(String relayState) {
this.relayState = relayState;
return this;
}
public SAML2PostBindingResponseBuilder destination(String destination) {
this.destination = destination;
return this;
}
public SAML2PostBindingResponseBuilder multiValuedRoles(boolean multiValuedRoles) {
this.multiValuedRoles = multiValuedRoles;
return this;
}
public SAML2PostBindingResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) {
this.disableAuthnStatement = disableAuthnStatement;
return this;
}
public Response error(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 build() throws ConfigurationException, ProcessingException, IOException {
Document responseDoc = getResponse();
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 {
Document samlResponseDocument = null;
ResponseType responseType = null;
SAML2Response saml2Response = new SAML2Response();
// Create a response type
String id = IDGenerator.create("ID_");
IssuerInfoHolder issuerHolder = new IssuerInfoHolder(responseIssuer);
issuerHolder.setStatusCode(JBossSAMLURIConstants.STATUS_SUCCESS.get());
IDPInfoHolder idp = new IDPInfoHolder();
idp.setNameIDFormatValue(userPrincipal);
idp.setNameIDFormat(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get());
SPInfoHolder sp = new SPInfoHolder();
sp.setResponseDestinationURI(destination);
sp.setRequestID(requestID);
sp.setIssuer(requestIssuer);
responseType = saml2Response.createResponseType(id, sp, idp, issuerHolder);
// Add information on the roles
AssertionType assertion = responseType.getAssertions().get(0).getAssertion();
// Create an AuthnStatementType
if (!disableAuthnStatement) {
String authContextRef = JBossSAMLURIConstants.AC_PASSWORD.get();
if (isNotNull(authMethod))
authContextRef = authMethod;
AuthnStatementType authnStatement = StatementUtil.createAuthnStatement(XMLTimeUtil.getIssueInstant(),
authContextRef);
authnStatement.setSessionIndex(assertion.getID());
assertion.addStatement(authnStatement);
}
if (roles != null && !roles.isEmpty()) {
AttributeStatementType attrStatement = StatementUtil.createAttributeStatementForRoles(roles, multiValuedRoles);
assertion.addStatement(attrStatement);
}
// Add in the attributes information
if (attributes != null && attributes.size() > 0) {
AttributeStatementType attStatement = StatementUtil.createAttributeStatement(attributes);
assertion.addStatement(attStatement);
}
try {
samlResponseDocument = saml2Response.convert(responseType);
if (logger.isTraceEnabled()) {
logger.trace("SAML Response Document: " + DocumentUtil.asString(samlResponseDocument));
}
} catch (Exception e) {
throw logger.samlAssertionMarshallError(e);
}
return samlResponseDocument;
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.protocol.saml;
import org.picketlink.common.PicketLinkLogger;
import org.picketlink.common.PicketLinkLoggerFactory;
import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request;
import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.picketlink.identity.federation.web.util.RedirectBindingUtil;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAMLRequestParser {
private static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
public static SAMLDocumentHolder parseRedirectBinding(String samlMessage) {
InputStream is;
is = RedirectBindingUtil.base64DeflateDecode(samlMessage);
SAML2Request saml2Request = new SAML2Request();
try {
saml2Request.getSAML2ObjectFromStream(is);
return saml2Request.getSamlDocumentHolder();
} catch (Exception e) {
logger.samlBase64DecodingError(e);
}
return null;
}
public static SAMLDocumentHolder parsePostBinding(String samlMessage) {
InputStream is;
byte[] samlBytes = PostBindingUtil.base64Decode(samlMessage);
is = new ByteArrayInputStream(samlBytes);
SAML2Request saml2Request = new SAML2Request();
try {
saml2Request.getSAML2ObjectFromStream(is);
return saml2Request.getSamlDocumentHolder();
} catch (Exception e) {
logger.samlBase64DecodingError(e);
}
return null;
}
}

View file

@ -0,0 +1,170 @@
package org.keycloak.protocol.saml;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
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.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.ProcessingException;
import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlLogin implements LoginProtocol {
protected static final Logger logger = Logger.getLogger(SamlLogin.class);
public static final String LOGIN_PROTOCOL = "saml";
public static final String SAML_BINDING = "saml_binding";
public static final String SAML_POST_BINDING = "post";
protected KeycloakSession session;
protected RealmModel realm;
protected HttpRequest request;
protected UriInfo uriInfo;
protected ClientConnection clientConnection;
@Override
public SamlLogin setSession(KeycloakSession session) {
this.session = session;
return this;
}
@Override
public SamlLogin setRealm(RealmModel realm) {
this.realm = realm;
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());
}
@Override
public Response invalidSessionError(ClientSessionModel clientSession) {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_AUTHNFAILED.get());
}
protected String getResponseIssuer(RealmModel realm) {
return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
}
protected Response getErrorResponse(ClientSessionModel clientSession, String status) {
String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);
String redirectUri = clientSession.getRedirectUri();
SAML2PostBindingResponseBuilder builder = new SAML2PostBindingResponseBuilder();
String responseIssuer = getResponseIssuer(realm);
builder .relayState(relayState)
.destination(redirectUri)
.responseIssuer(responseIssuer)
.requestIssuer(clientSession.getClient().getClientId());
try {
return builder.error(status);
} catch (Exception e) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response");
}
}
@Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) {
ClientSessionModel clientSession = accessCode.getClientSession();
if (SamlLogin.SAML_POST_BINDING.equals(clientSession.getNote(SamlLogin.SAML_BINDING))) {
return postBinding(userSession, clientSession);
}
throw new RuntimeException("still need to implement redirect binding");
}
protected Response postBinding(UserSessionModel userSession, ClientSessionModel clientSession) {
String requestID = clientSession.getNote("REQUEST_ID");
String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE);
String redirectUri = clientSession.getRedirectUri();
String responseIssuer = getResponseIssuer(realm);
SAML2PostBindingResponseBuilder builder = new SAML2PostBindingResponseBuilder();
builder.requestID(requestID)
.relayState(relayState)
.destination(redirectUri)
.responseIssuer(responseIssuer)
.requestIssuer(clientSession.getClient().getClientId())
.userPrincipal(userSession.getUser().getUsername()) // todo userId instead? There is no username claim it seems
.attribute(X500SAMLProfileConstants.USERID.getFriendlyName(), userSession.getUser().getId())
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
initClaims(builder, clientSession.getClient(), userSession.getUser());
if (clientSession.getRoles() != null) {
for (String roleId : clientSession.getRoles()) {
// todo need a role mapping
RoleModel roleModel = clientSession.getRealm().getRoleById(roleId);
builder.roles(roleModel.getName());
}
}
try {
return builder.build();
} catch (Exception e) {
logger.error("failed", e);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response");
}
}
public void initClaims(SAML2PostBindingResponseBuilder builder, ClientModel model, UserModel user) {
if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) {
builder.attribute(X500SAMLProfileConstants.EMAIL_ADDRESS.getFriendlyName(), user.getEmail());
}
if (ClaimMask.hasName(model.getAllowedClaimsMask())) {
builder.attribute(X500SAMLProfileConstants.GIVEN_NAME.getFriendlyName(), user.getFirstName());
builder.attribute(X500SAMLProfileConstants.SURNAME.getFriendlyName(), user.getLastName());
}
}
@Override
public Response consentDenied(ClientSessionModel clientSession) {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,43 @@
package org.keycloak.protocol.saml;
import org.keycloak.Config;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.services.managers.AuthenticationManager;
import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlLoginFactory implements LoginProtocolFactory {
@Override
public Object createProtocolEndpoint(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
return new SamlService(realm, event, authManager);
}
@Override
public LoginProtocol create(KeycloakSession session) {
return new SamlLogin().setSession(session);
}
@Override
public void init(Config.Scope config) {
PicketLinkCoreSTS sts = PicketLinkCoreSTS.instance();
sts.installDefaultConfiguration();
}
@Override
public void close() {
}
@Override
public String getId() {
return "saml";
}
}

View file

@ -0,0 +1,246 @@
package org.keycloak.protocol.saml;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OpenIDConnectService;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.flows.Flows;
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 javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
/**
* Resource class for the oauth/openid connect token service
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlService {
protected static final Logger logger = Logger.getLogger(SamlService.class);
protected RealmModel realm;
private EventBuilder event;
protected AuthenticationManager authManager;
@Context
protected Providers providers;
@Context
protected SecurityContext securityContext;
@Context
protected UriInfo uriInfo;
@Context
protected HttpHeaders headers;
@Context
protected HttpRequest request;
@Context
protected HttpResponse response;
@Context
protected KeycloakSession session;
@Context
protected ClientConnection clientConnection;
/*
@Context
protected ResourceContext resourceContext;
*/
public SamlService(RealmModel realm, EventBuilder event, AuthenticationManager authManager) {
this.realm = realm;
this.event = event;
this.authManager = authManager;
}
/**
*/
@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);
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required");
}
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled");
}
if (samlRequest == null) {
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
}
SAMLDocumentHolder documentHolder = SAMLRequestParser.parsePostBinding(samlRequest);
if (documentHolder == null) {
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
}
SAML2Object samlObject = documentHolder.getSamlObject();
if (!(samlObject instanceof AuthnRequestType)) {
event.error(Errors.INVALID_TOKEN);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request");
}
// Get the SAML Request Message
AuthnRequestType requestAbstractType = (AuthnRequestType) samlObject;
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");
}
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
if (redirect == null) {
event.error(Errors.INVALID_REDIRECT_URI);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri.");
}
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlLogin.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
clientSession.setNote(SamlLogin.SAML_BINDING, SamlLogin.SAML_POST_BINDING);
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
clientSession.setNote("REQUEST_ID", requestAbstractType.getID());
Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
if (response != null) return response;
LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo)
.setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode());
String rememberMeUsername = null;
if (realm.isRememberMe()) {
Cookie rememberMeCookie = headers.getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
if (rememberMeCookie != null && !"".equals(rememberMeCookie.getValue())) {
rememberMeUsername = rememberMeCookie.getValue();
}
}
if (rememberMeUsername != null) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<String, String>();
formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
formData.add("rememberMe", "on");
forms.setFormData(formData);
}
return forms.createLogin();
}
/**
* 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);
}
// 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());
}
if (redirectUri != null) {
String validatedRedirect = OpenIDConnectService.verifyRealmRedirectUri(uriInfo, redirectUri, realm);
if (validatedRedirect == null) {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri.");
}
return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build();
} else {
return Response.ok().build();
}
}
private void logout(UserSessionModel userSession) {
authManager.logout(session, realm, userSession, uriInfo, clientConnection);
event.user(userSession.getUser()).session(userSession).success();
}
private boolean checkSsl() {
if (uriInfo.getBaseUri().getScheme().equals("https")) {
return true;
} else {
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();
}
}

View file

@ -0,0 +1 @@
org.keycloak.protocol.saml.SamlLoginFactory

View file

@ -16,5 +16,8 @@
<module name="org.keycloak.keycloak-undertow-adapter" />
<module name="org.keycloak.keycloak-as7-adapter" />
</exclusions>
<exclude-subsystems>
<subsystem name="webservices"/>
</exclude-subsystems>
</deployment>
</jboss-deployment-structure>

View file

@ -18,15 +18,15 @@ import javax.ws.rs.core.UriInfo;
* @version $Revision: 1 $
*/
public interface LoginProtocol extends Provider {
OpenIDConnect setSession(KeycloakSession session);
LoginProtocol setSession(KeycloakSession session);
OpenIDConnect setRealm(RealmModel realm);
LoginProtocol setRealm(RealmModel realm);
OpenIDConnect setRequest(HttpRequest request);
LoginProtocol setRequest(HttpRequest request);
OpenIDConnect setUriInfo(UriInfo uriInfo);
LoginProtocol setUriInfo(UriInfo uriInfo);
OpenIDConnect setClientConnection(ClientConnection clientConnection);
LoginProtocol setClientConnection(ClientConnection clientConnection);
Response cancelLogin(ClientSessionModel clientSession);
Response invalidSessionError(ClientSessionModel clientSession);

View file

@ -0,0 +1,48 @@
{
"id": "demo",
"realm": "demo",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": true,
"resetPasswordAllowed": true,
"passwordCredentialGrantAllowed": true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"smtpServer": {
"from": "auto@keycloak.org",
"host": "localhost",
"port":"3025"
},
"users" : [
{
"username" : "bburke",
"enabled": true,
"email" : "bburke@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["manager"]
}
],
"applications": [
{
"name": "http://localhost:8080/sales-post/",
"enabled": true,
"fullScopeAllowed": true,
"redirectUris": [
"http://localhost:8080/sales-post/*"
]
}
],
"roles" : {
"realm" : [
{
"name": "manager",
"description": "Have Manager privileges"
}
]
}
}