saml basic
This commit is contained in:
parent
6cf62a2768
commit
e3a3933390
13 changed files with 1102 additions and 6 deletions
11
dependencies/server-all/pom.xml
vendored
11
dependencies/server-all/pom.xml
vendored
|
@ -138,6 +138,17 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</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 -->
|
<!-- mongo -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
|
|
8
pom.xml
8
pom.xml
|
@ -18,7 +18,7 @@
|
||||||
<resteasy.version>2.3.7.Final</resteasy.version>
|
<resteasy.version>2.3.7.Final</resteasy.version>
|
||||||
<resteasy.version.latest>3.0.9.Final</resteasy.version.latest>
|
<resteasy.version.latest>3.0.9.Final</resteasy.version.latest>
|
||||||
<undertow.version>1.0.15.Final</undertow.version>
|
<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>
|
<picketbox.ldap.version>1.0.2.Final</picketbox.ldap.version>
|
||||||
<mongo.driver.version>2.11.3</mongo.driver.version>
|
<mongo.driver.version>2.11.3</mongo.driver.version>
|
||||||
<jboss.logging.version>3.1.4.GA</jboss.logging.version>
|
<jboss.logging.version>3.1.4.GA</jboss.logging.version>
|
||||||
|
@ -107,6 +107,7 @@
|
||||||
<module>picketlink</module>
|
<module>picketlink</module>
|
||||||
<module>federation</module>
|
<module>federation</module>
|
||||||
<module>services</module>
|
<module>services</module>
|
||||||
|
<module>saml</module>
|
||||||
<module>social</module>
|
<module>social</module>
|
||||||
<module>forms</module>
|
<module>forms</module>
|
||||||
<module>examples</module>
|
<module>examples</module>
|
||||||
|
@ -229,6 +230,11 @@
|
||||||
<artifactId>picketlink-idm-impl</artifactId>
|
<artifactId>picketlink-idm-impl</artifactId>
|
||||||
<version>${picketlink.version}</version>
|
<version>${picketlink.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.picketlink</groupId>
|
||||||
|
<artifactId>picketlink-federation</artifactId>
|
||||||
|
<version>${picketlink.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.picketlink</groupId>
|
<groupId>org.picketlink</groupId>
|
||||||
<artifactId>picketlink-idm-simple-schema</artifactId>
|
<artifactId>picketlink-idm-simple-schema</artifactId>
|
||||||
|
|
20
saml/pom.xml
Executable file
20
saml/pom.xml
Executable 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
55
saml/saml-core/pom.xml
Executable 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
128
saml/saml-protocol/pom.xml
Executable 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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
170
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLogin.java
Executable file
170
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlLogin.java
Executable 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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
246
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
Executable file
246
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
Executable 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
org.keycloak.protocol.saml.SamlLoginFactory
|
|
@ -18,15 +18,15 @@ import javax.ws.rs.core.UriInfo;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface LoginProtocol extends Provider {
|
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 cancelLogin(ClientSessionModel clientSession);
|
||||||
Response invalidSessionError(ClientSessionModel clientSession);
|
Response invalidSessionError(ClientSessionModel clientSession);
|
||||||
|
|
48
testsuite/integration/src/test/resources/testsaml.json
Executable file
48
testsuite/integration/src/test/resources/testsaml.json
Executable 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue