Commit d990b7a9 authored by gilles.grandgerard's avatar gilles.grandgerard

Bascule v1 -> v2

parent 92ea6b15
......@@ -13,7 +13,7 @@ services:
- docker
env:
- KEYCLOAK_VERSION=8.0.0
- KEYCLOAK_VERSION=9.0.2
before_install:
- if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then docker pull quay.io/keycloak/keycloak:$KEYCLOAK_VERSION; fi
......
......@@ -47,5 +47,5 @@ It is licensed under the Apache License 2.0.
## References
[1] http://www.keycloak.org
[2] https://issues.jboss.org/browse/KEYCLOAK-1047 (Support CAS 2.0 SSO protocol)
[3] https://apereo.github.io/cas/4.2.x/protocol/CAS-Protocol-Specification.html
[3] https://apereo.github.io/cas/6.1.x/protocol/CAS-Protocol-Specification.html
[4] https://keycloak.gitbooks.io/server-developer-guide/content/topics/providers.html
#!/bin/bash -x
SCRIPT_PATH="${BASH_SOURCE[0]}"
if [ -h "${SCRIPT_PATH}" ]
then
while [ -h "${SCRIPT_PATH}" ]
do
SCRIPT_PATH=$(readlink "${SCRIPT_PATH}")
done
fi
pushd . > /dev/null
DIR_SCRIPT=$(dirname "${SCRIPT_PATH}" )
cd "${DIR_SCRIPT}" > /dev/null || exit 1
SCRIPT_PATH=$(pwd);
popd > /dev/null || exit 1
WORKSPACE=$SCRIPT_PATH
if [ -z "$JAVA_HOME" ]
then
JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
JAVA_HOME=/home/gilles/jdk1.7.0_121
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
fi
export JAVA_HOME
if [ -z "$M2_HOME" ]
then
M2_HOME=~/.m2
fi
if [ "$LOGNAME" == "jenkins" ]
then
MAVEN_HOME=/var/lib/jenkins/tools/hudson.tasks.Maven_MavenInstallation/maven_3.6.3
fi
if [ -z "$MAVEN_HOME" ]
then
MAVEN_HOME=~/apache-maven-3.6.3
fi
export MAVEN_HOME
MAVEN_PATH=$MAVEN_HOME/bin
PATH=$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH
export PATH
echo "WORKSPACE: $WORKSPACE"
echo "MAVEN_HOME: $MAVEN_HOME"
echo "JAVA_HOME: $JAVA_HOME"
echo "PATH: $PATH"
$JAVA_HOME/bin/java -version
pushd . > /dev/null
cd "${WORKSPACE}" > /dev/null || exit 1
mvn -e clean compile install
popd > /dev/null || exit 1
<FindBugsFilter>
<Match>
<Or>
<!-- Used for backward compatibility and extending utility classes -->
<Bug pattern="NM_SAME_SIMPLE_NAME_AS_SUPERCLASS"/>
<!-- Various debug probes have non final static fields -->
<!-- TODO: Replace by in-code annotations -->
<Bug pattern="MS_SHOULD_BE_FINAL"/>
<!-- Groovy generates this -->
<Bug pattern="UMAC_UNCALLABLE_METHOD_OF_ANONYMOUS_CLASS"/>
<!-- TODO: It's a real issue, but the code is flooded by it right now-->
<Bug pattern="DM_DEFAULT_ENCODING"/>
</Or>
</Match>
</FindBugsFilter>
......@@ -22,7 +22,7 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-protocol-cas</artifactId>
<version>8.0.0</version>
<version>9.0.2</version>
<name>Keycloak CAS Protocol</name>
<description />
......
......@@ -5,12 +5,18 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.cas.endpoints.*;
import org.keycloak.protocol.cas.endpoints.AuthorizationEndpoint;
import org.keycloak.protocol.cas.endpoints.LogoutEndpoint;
import org.keycloak.protocol.cas.endpoints.ValidateEndpoint;
import org.keycloak.protocol.cas.endpoints.SamlValidateEndpoint;
import org.keycloak.protocol.cas.endpoints.ServiceValidateEndpoint;
import org.keycloak.services.resources.RealmsResource;
import javax.ws.rs.Path;
import javax.ws.rs.core.*;
// cf. https://apereo.github.io/cas/6.1.x/protocol/CAS-Protocol-Specification.html
public class CASLoginProtocolService {
private RealmModel realm;
private EventBuilder event;
......
......@@ -5,7 +5,15 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.cas.CASLoginProtocol;
import org.keycloak.protocol.cas.mappers.CASAttributeMapper;
......@@ -152,8 +160,10 @@ public abstract class AbstractValidateEndpoint {
protected Map<String, Object> getUserAttributes() {
UserSessionModel userSession = clientSession.getUserSession();
// CAS protocol does not support scopes, so pass null scopeParam
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null);
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, clientSession.getNote(CASLoginProtocol.SESSION_SERVICE_TICKET), session);
//ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null);
Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
......
package org.keycloak.protocol.cas.endpoints;
import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel;
......@@ -57,14 +57,14 @@ public class SamlValidateEndpoint extends AbstractValidateEndpoint {
Map<String, Object> attributes = getUserAttributes();
SAML11ResponseType response = SamlResponseHelper.successResponse(issuer, user.getUsername(), attributes);
ResponseType response = SamlResponseHelper.successResponse(issuer, user.getUsername(), attributes);
return Response.ok(SamlResponseHelper.soap(response)).build();
} catch (CASValidationException ex) {
logger.warnf("Invalid SAML1.1 token %s", ex.getErrorDescription());
SAML11ResponseType response = SamlResponseHelper.errorResponse(ex);
ResponseType response = SamlResponseHelper.errorResponse(ex);
return Response.ok().entity(SamlResponseHelper.soap(response)).build();
}
}
......
package org.keycloak.protocol.cas.endpoints;
import java.util.Map;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.cas.mappers.CASAttributeMapper;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.cas.representations.CASServiceResponse;
import org.keycloak.protocol.cas.utils.CASValidationException;
import org.keycloak.protocol.cas.utils.ContentTypeHelper;
import org.keycloak.protocol.cas.utils.ServiceResponseHelper;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.util.DefaultClientSessionContext;
import javax.ws.rs.core.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ServiceValidateEndpoint extends ValidateEndpoint {
@Context
......
package org.keycloak.protocol.cas.representations;
public enum CASErrorCode {
public enum CASErrorCode
{
/** not all of the required request parameters were present */
INVALID_REQUEST,
/** failure to meet the requirements of validation specification */
......
package org.keycloak.protocol.cas.representations;
import org.keycloak.dom.saml.v1.assertion.*;
import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
import org.keycloak.dom.saml.v1.protocol.SAML11StatusCodeType;
import org.keycloak.dom.saml.v1.protocol.SAML11StatusType;
import org.keycloak.protocol.cas.utils.CASValidationException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.saml.v1.SAML11Constants;
import org.keycloak.saml.processing.core.saml.v1.writers.SAML11ResponseWriter;
import org.keycloak.services.validation.Validation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.StringWriter;
import java.net.URI;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
......@@ -28,14 +29,28 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.net.URI;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.ConditionsType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.StatementAbstractType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.dom.saml.v2.protocol.StatusCodeType;
import org.keycloak.dom.saml.v2.protocol.StatusType;
import org.keycloak.protocol.cas.utils.CASValidationException;
import org.keycloak.saml.SAML2ErrorResponseBuilder;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.api.saml.v2.response.SAML2Response;
import org.keycloak.saml.processing.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
import org.keycloak.services.validation.Validation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
public class SamlResponseHelper {
private final static DatatypeFactory factory;
......@@ -48,33 +63,50 @@ public class SamlResponseHelper {
}
}
public static SAML11ResponseType errorResponse(CASValidationException ex) {
public static ResponseType errorResponse(CASValidationException ex) {
// GG return JBossSAMLAuthnResponseFactory.createResponseType("_" + UUID.randomUUID().toString(),
// "urn:oasis:names:tc:SAML:2.0:status:RequestDenied")
// );
// GG return new SAML2ErrorResponseBuilder()
// .status("urn:oasis:names:tc:SAML:2.0:status:RequestDenied")
// .buildDocument();
//
ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC);
XMLGregorianCalendar now = factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned));
return applyTo(new SAML11ResponseType("_" + UUID.randomUUID().toString(), now), obj -> {
obj.setStatus(applyTo(new SAML11StatusType(), status -> {
status.setStatusCode(new SAML11StatusCodeType(QName.valueOf("samlp:RequestDenied")));
return applyTo(new ResponseType("_" + UUID.randomUUID().toString(), now), obj -> {
obj.setStatus(applyTo(new StatusType(), status -> {
//status.setStatusCode(new StatusCodeType(QName.valueOf("samlp:RequestDenied")));
status.setStatusCode(applyTo( new StatusCodeType(), statusCode -> {
statusCode.setValue( URI.create("urn:oasis:names:tc:SAML:2.0:status:RequestDenied"));
}));
status.setStatusMessage(ex.getErrorDescription());
}));
});
}
public static SAML11ResponseType successResponse(String issuer, String username, Map<String, Object> attributes) {
ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC);
public static ResponseType successResponse(String issuer, String username, Map<String, Object> attributes) {
//GG return JBossSAMLAuthnResponseFactory.createResponseType("_" + UUID.randomUUID().toString(), "urn:oasis:names:tc:SAML:2.0:status:RequestDenied")
// JBossSAMLAuthnResponseFactory.createResponseType();
// idée saml2 ! SAML2Response.createResponseType() ;
ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC);
XMLGregorianCalendar now = factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned));
return applyTo(new SAML11ResponseType("_" + UUID.randomUUID().toString(), now),
return applyTo(new ResponseType("_" + UUID.randomUUID().toString(), now),
obj -> {
obj.setStatus(applyTo(new SAML11StatusType(), status -> status.setStatusCode(SAML11StatusCodeType.SUCCESS)));
obj.add(applyTo(new SAML11AssertionType("_" + UUID.randomUUID().toString(), now), assertion -> {
obj.setStatus(applyTo(new StatusType(), status -> status.setStatusCode(StatusCodeType.SUCCESS)));
obj.add(applyTo(new AssertionType("_" + UUID.randomUUID().toString(), now), assertion -> {
assertion.setIssuer(issuer);
assertion.setConditions(applyTo(new SAML11ConditionsType(), conditions -> {
assertion.setConditions(applyTo(new ConditionsType(), conditions -> {
conditions.setNotBefore(now);
conditions.setNotOnOrAfter(factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned.plusMinutes(5))));
}));
assertion.add(applyTo(new SAML11AuthenticationStatementType(
URI.create(SAML11Constants.AUTH_METHOD_PASSWORD),
assertion.add(applyTo(new AuthenticationStatementType(
URI.create(
"urn:oasis:names:tc:SAML:1.0:am:password"
//Constants.AUTH_METHOD_PASSWORD
),
now
), stmt -> stmt.setSubject(toSubject(username))));
assertion.addAllStatements(toAttributes(username, attributes));
......@@ -83,13 +115,13 @@ public class SamlResponseHelper {
);
}
private static List<SAML11StatementAbstractType> toAttributes(String username, Map<String, Object> attributes) {
List<SAML11AttributeType> converted = attributeElements(attributes);
private static List<StatementAbstractType> toAttributes(String username, Map<String, Object> attributes) {
List<AttributeType> converted = attributeElements(attributes);
if (converted.isEmpty()) {
return Collections.emptyList();
}
return Collections.singletonList(applyTo(
new SAML11AttributeStatementType(),
new AttributeStatementType(),
attrs -> {
attrs.setSubject(toSubject(username));
attrs.addAllAttributes(converted);
......@@ -97,13 +129,13 @@ public class SamlResponseHelper {
);
}
private static List<SAML11AttributeType> attributeElements(Map<String, Object> attributes) {
private static List<AttributeType> attributeElements(Map<String, Object> attributes) {
return attributes.entrySet().stream().flatMap(e ->
toAttribute(e.getKey(), e.getValue())
).filter(a -> !a.get().isEmpty()).collect(Collectors.toList());
}
private static Stream<SAML11AttributeType> toAttribute(String name, Object value) {
private static Stream<AttributeType> toAttribute(String name, Object value) {
if (name == null || value == null) {
return Stream.empty();
}
......@@ -114,9 +146,9 @@ public class SamlResponseHelper {
return Stream.of(samlAttribute(name, Collections.singletonList(value.toString())));
}
private static SAML11AttributeType samlAttribute(String name, List<Object> listString) {
private static AttributeType samlAttribute(String name, List<Object> listString) {
return applyTo(
new SAML11AttributeType(name, URI.create("http://www.ja-sig.org/products/cas/")),
new AttributeType(name, URI.create("http://www.ja-sig.org/products/cas/")),
attr -> attr.addAll(listString)
);
}
......@@ -125,13 +157,13 @@ public class SamlResponseHelper {
return value.stream().map(Object::toString).collect(Collectors.toList());
}
private static SAML11SubjectType toSubject(String username) {
private static SubjectType toSubject(String username) {
return applyTo(
new SAML11SubjectType(),
new SubjectType(),
subject -> subject.setChoice(
new SAML11SubjectType.SAML11SubjectTypeChoice(
new SubjectType.STSubType(
applyTo(
new SAML11NameIdentifierType(username),
new NameIDType(username),
ctype -> ctype.setFormat(nameIdFormat(username))
)
)
......@@ -141,8 +173,11 @@ public class SamlResponseHelper {
private static URI nameIdFormat(String username) {
return URI.create(Validation.isEmailValid(username) ?
SAML11Constants.FORMAT_EMAIL_ADDRESS :
SAML11Constants.FORMAT_UNSPECIFIED
//Constants.FORMAT_EMAIL_ADDRESS
"urn:oasis:names:tc:SAML:1.1:nameid-­format:emailAddress"
:
//Constants.FORMAT_UNSPECIFIED
"urn:oasis:names:tc:SAML:1.1:nameid­-format:unspecified"
);
}
......@@ -151,7 +186,7 @@ public class SamlResponseHelper {
return input;
}
public static String soap(SAML11ResponseType response) {
public static String soap(ResponseType response) {
try {
Document result = toDOM(response);
......@@ -163,7 +198,7 @@ public class SamlResponseHelper {
}
}
public static Document toDOM(SAML11ResponseType response) throws ParserConfigurationException, XMLStreamException, ProcessingException {
public static Document toDOM(ResponseType response) throws ParserConfigurationException, XMLStreamException, ProcessingException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
......@@ -172,7 +207,7 @@ public class SamlResponseHelper {
Document doc = dbf.newDocumentBuilder().newDocument();
DOMResult result = new DOMResult(doc);
XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(result);
SAML11ResponseWriter writer = new SAML11ResponseWriter(xmlWriter);
SAMLResponseWriter writer = new SAMLResponseWriter(xmlWriter);
writer.write(response);
return doc;
}
......
package org.keycloak.protocol.cas;
import org.junit.Test;
import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.protocol.cas.representations.CASErrorCode;
import org.keycloak.protocol.cas.representations.SamlResponseHelper;
import org.keycloak.protocol.cas.utils.CASValidationException;
import org.keycloak.protocol.cas.XMLValidator;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
......@@ -16,7 +17,7 @@ import static org.junit.Assert.assertTrue;
public class SamlResponseTest {
@Test
public void successResponseIsWrappedInSOAP() {
SAML11ResponseType response = SamlResponseHelper.successResponse("keycloak", "test@example.com", Collections.emptyMap());
ResponseType response = SamlResponseHelper.successResponse("keycloak", "test@example.com", Collections.emptyMap());
String soapResult = SamlResponseHelper.soap(response);
assertTrue(soapResult.contains("samlp:Success"));
assertTrue(soapResult.contains("test@example.com"));
......@@ -25,14 +26,14 @@ public class SamlResponseTest {
@Test
public void failureResponseIsWrappedInSOAP() {
SAML11ResponseType response = SamlResponseHelper.errorResponse(new CASValidationException(CASErrorCode.INVALID_TICKET, "Nope", Response.Status.BAD_REQUEST));
ResponseType response = SamlResponseHelper.errorResponse(new CASValidationException(CASErrorCode.INVALID_TICKET, "Nope", Response.Status.BAD_REQUEST));
String nope = SamlResponseHelper.soap(response);
assertTrue(nope.contains("Nope"));
}
@Test
public void validateSchemaResponseFailure() throws Exception {
SAML11ResponseType response = SamlResponseHelper.errorResponse(new CASValidationException(CASErrorCode.INVALID_TICKET, "Nope", Response.Status.BAD_REQUEST));
ResponseType response = SamlResponseHelper.errorResponse(new CASValidationException(CASErrorCode.INVALID_TICKET, "Nope", Response.Status.BAD_REQUEST));
String output = SamlResponseHelper.toString(SamlResponseHelper.toDOM(response));
Document doc = XMLValidator.parseAndValidate(output, XMLValidator.schemaFromClassPath("oasis-sstc-saml-schema-protocol-1.1.xsd"));
assertNotNull(doc);
......@@ -40,7 +41,7 @@ public class SamlResponseTest {
@Test
public void validateSchemaResponseSuccess() throws Exception {
SAML11ResponseType response = SamlResponseHelper.successResponse("keycloak", "test@example.com", Collections.emptyMap());
ResponseType response = SamlResponseHelper.successResponse("keycloak", "test@example.com", Collections.emptyMap());
String output = SamlResponseHelper.toString(SamlResponseHelper.toDOM(response));
Document doc = XMLValidator.parseAndValidate(output, XMLValidator.schemaFromClassPath("oasis-sstc-saml-schema-protocol-1.1.xsd"));
assertNotNull(doc);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment