From 74023ad339616936c5a2415f3b0347858c38df18 Mon Sep 17 00:00:00 2001
From: Erlend Hamnaberg <erlend@hamnaberg.net>
Date: Fri, 30 Nov 2018 16:46:52 +0000
Subject: [PATCH] Saml 1.1 Validate support
---
src/main/java/org/keycloak/protocol/cas/representations/SamlResponseHelper.java | 208 ++++++++++
src/test/java/org/keycloak/protocol/cas/SamlResponseTest.java | 48 ++
src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java | 12
src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-assertion-1.1.xsd | 201 ++++++++++
src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java | 14
src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-protocol-1.1.xsd | 132 ++++++
pom.xml | 7
src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java | 30 -
src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java | 175 +++++++++
src/test/java/org/keycloak/protocol/cas/XMLValidator.java | 35 +
src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java | 107 +++++
src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java | 150 -------
src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java | 1
13 files changed, 930 insertions(+), 190 deletions(-)
diff --git a/pom.xml b/pom.xml
index ff7af80..e70a469 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,6 +61,7 @@
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
@@ -85,7 +86,7 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core</artifactId>
<version>${keycloak.version}</version>
- <scope>test</scope>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
@@ -111,7 +112,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
- <version>3.1</version>
+ <version>3.8.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
@@ -126,7 +127,7 @@
<configuration>
<archive>
<manifestEntries>
- <Dependencies>javax.xml.bind.api,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services</Dependencies>
+ <Dependencies>javax.xml.bind.api,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.keycloak.keycloak-saml-core,org.keycloak.keycloak-saml-core-public</Dependencies>
</manifestEntries>
</archive>
</configuration>
diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
index 2723079..fecd557 100644
--- a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
+++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocol.java
@@ -25,6 +25,7 @@
public static final String LOGIN_PROTOCOL = "cas";
public static final String SERVICE_PARAM = "service";
+ public static final String TARGET_PARAM = "TARGET";
public static final String RENEW_PARAM = "renew";
public static final String GATEWAY_PARAM = "gateway";
public static final String TICKET_PARAM = "ticket";
diff --git a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
index 861742a..80e5c28 100644
--- a/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
+++ b/src/main/java/org/keycloak/protocol/cas/CASLoginProtocolService.java
@@ -5,10 +5,7 @@
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
-import org.keycloak.protocol.cas.endpoints.AuthorizationEndpoint;
-import org.keycloak.protocol.cas.endpoints.LogoutEndpoint;
-import org.keycloak.protocol.cas.endpoints.ServiceValidateEndpoint;
-import org.keycloak.protocol.cas.endpoints.ValidateEndpoint;
+import org.keycloak.protocol.cas.endpoints.*;
import org.keycloak.services.resources.RealmsResource;
import javax.ws.rs.Path;
@@ -57,6 +54,13 @@
return endpoint;
}
+ @Path("samlValidate")
+ public Object validateSaml11() {
+ SamlValidateEndpoint endpoint = new SamlValidateEndpoint(realm, event);
+ ResteasyProviderFactory.getInstance().injectProperties(endpoint);
+ return endpoint;
+ }
+
@Path("serviceValidate")
public Object serviceValidate() {
ServiceValidateEndpoint endpoint = new ServiceValidateEndpoint(realm, event);
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java
new file mode 100644
index 0000000..ecec352
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/AbstractValidateEndpoint.java
@@ -0,0 +1,175 @@
+package org.keycloak.protocol.cas.endpoints;
+
+import org.jboss.logging.Logger;
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.*;
+import org.keycloak.protocol.ProtocolMapper;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.cas.mappers.CASAttributeMapper;
+import org.keycloak.protocol.cas.representations.CASErrorCode;
+import org.keycloak.protocol.cas.utils.CASValidationException;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.util.DefaultClientSessionContext;
+
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class AbstractValidateEndpoint {
+ protected final Logger logger = Logger.getLogger(getClass());
+ @Context
+ protected KeycloakSession session;
+ @Context
+ protected ClientConnection clientConnection;
+ @Context
+ protected HttpRequest request;
+ @Context
+ protected HttpHeaders headers;
+ protected RealmModel realm;
+ protected EventBuilder event;
+ protected ClientModel client;
+ protected AuthenticatedClientSessionModel clientSession;
+
+ public AbstractValidateEndpoint(RealmModel realm, EventBuilder event) {
+ this.realm = realm;
+ this.event = event;
+ }
+
+ protected void checkSsl() {
+ if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
+ throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
+ }
+ }
+
+ protected void checkRealm() {
+ if (!realm.isEnabled()) {
+ throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Realm not enabled", Response.Status.FORBIDDEN);
+ }
+ }
+
+ protected void checkClient(String service) {
+ if (service == null) {
+ event.error(Errors.INVALID_REQUEST);
+ throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.SERVICE_PARAM, Response.Status.BAD_REQUEST);
+ }
+
+ client = realm.getClients().stream()
+ .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol()))
+ .filter(c -> RedirectUtils.verifyRedirectUri(session.getContext().getUri(), service, realm, c) != null)
+ .findFirst().orElse(null);
+ if (client == null) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client not found", Response.Status.BAD_REQUEST);
+ }
+
+ if (!client.isEnabled()) {
+ event.error(Errors.CLIENT_DISABLED);
+ throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client disabled", Response.Status.BAD_REQUEST);
+ }
+
+ event.client(client.getClientId());
+
+ session.getContext().setClient(client);
+ }
+
+ protected void checkTicket(String ticket, boolean requireReauth) {
+ if (ticket == null) {
+ event.error(Errors.INVALID_CODE);
+ throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST);
+ }
+ if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) {
+ event.error(Errors.INVALID_CODE);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST);
+ }
+
+ String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length());
+
+ String[] parts = code.split("\\.");
+ if (parts.length == 4) {
+ event.detail(Details.CODE_ID, parts[2]);
+ }
+
+ ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, null, session, realm, client, event, AuthenticatedClientSessionModel.class);
+ if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
+ event.error(Errors.INVALID_CODE);
+
+ // Attempt to use same code twice should invalidate existing clientSession
+ AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
+ if (clientSession != null) {
+ clientSession.detachFromUserSession();
+ }
+
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST);
+ }
+
+ clientSession = parseResult.getClientSession();
+
+ if (parseResult.isExpiredToken()) {
+ event.error(Errors.EXPIRED_CODE);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST);
+ }
+
+ clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket);
+
+ if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) {
+ event.error(Errors.SESSION_EXPIRED);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST);
+ }
+
+ UserSessionModel userSession = clientSession.getUserSession();
+
+ if (userSession == null) {
+ event.error(Errors.USER_SESSION_NOT_FOUND);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST);
+ }
+
+ UserModel user = userSession.getUser();
+ if (user == null) {
+ event.error(Errors.USER_NOT_FOUND);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User not found", Response.Status.BAD_REQUEST);
+ }
+ if (!user.isEnabled()) {
+ event.error(Errors.USER_DISABLED);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User disabled", Response.Status.BAD_REQUEST);
+ }
+
+ event.user(userSession.getUser());
+ event.session(userSession.getId());
+
+ if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
+ event.error(Errors.INVALID_CODE);
+ throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST);
+ }
+
+ if (!AuthenticationManager.isSessionValid(realm, userSession)) {
+ event.error(Errors.USER_SESSION_NOT_FOUND);
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST);
+ }
+ }
+
+ 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);
+
+ Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
+ KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+ Map<String, Object> attributes = new HashMap<>();
+ for (ProtocolMapperModel mapping : mappings) {
+ ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
+ if (mapper instanceof CASAttributeMapper) {
+ ((CASAttributeMapper) mapper).setAttribute(attributes, mapping, userSession, session, clientSessionCtx);
+ }
+ }
+ return attributes;
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java
new file mode 100644
index 0000000..3d7f3c3
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/SamlValidateEndpoint.java
@@ -0,0 +1,107 @@
+package org.keycloak.protocol.cas.endpoints;
+
+import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.cas.representations.CASErrorCode;
+import org.keycloak.protocol.cas.representations.SamlResponseHelper;
+import org.keycloak.protocol.cas.utils.CASValidationException;
+import org.keycloak.services.Urls;
+import org.xml.sax.InputSource;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.StringReader;
+import java.util.*;
+
+import static org.keycloak.protocol.cas.CASLoginProtocol.TARGET_PARAM;
+
+public class SamlValidateEndpoint extends AbstractValidateEndpoint {
+ public SamlValidateEndpoint(RealmModel realm, EventBuilder event) {
+ super(realm, event.event(EventType.CODE_TO_TOKEN));
+ }
+
+ @POST
+ @Consumes("text/xml;charset=utf-8")
+ @Produces("text/xml;charset=utf-8")
+ public Response validate(String input) {
+ MultivaluedMap<String, String> queryParams = request.getUri().getQueryParameters();
+ try {
+ String soapAction = Optional.ofNullable(request.getHttpHeaders().getHeaderString("SOAPAction")).map(s -> s.trim().replace("\"", "")).orElse("");
+ if (!soapAction.equals("http://www.oasis-open.org/committees/security")) {
+ throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Not a validation request", Response.Status.BAD_REQUEST);
+ }
+
+ String service = queryParams.getFirst(TARGET_PARAM);
+ boolean renew = queryParams.containsKey(CASLoginProtocol.RENEW_PARAM);
+
+ checkRealm();
+ checkSsl();
+ checkClient(service);
+ String issuer = Urls.realmIssuer(request.getUri().getBaseUri(), realm.getName());
+ String ticket = getTicket(input);
+
+ checkTicket(ticket, renew);
+ UserModel user = clientSession.getUserSession().getUser();
+
+ Map<String, Object> attributes = getUserAttributes();
+
+ SAML11ResponseType 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);
+ return Response.ok().entity(SamlResponseHelper.soap(response)).build();
+ }
+ }
+
+ private String getTicket(String input) {
+ try {
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ xPath.setNamespaceContext(new MapNamespaceContext(Collections.singletonMap("samlp", "urn:oasis:names:tc:SAML:1.0:protocol")));
+
+ XPathExpression expression = xPath.compile("//samlp:AssertionArtifact/text()");
+
+ return expression.evaluate(new InputSource(new StringReader(input)));
+ } catch (XPathExpressionException ex) {
+ throw new CASValidationException(CASErrorCode.INVALID_TICKET, ex.getMessage(), Response.Status.BAD_REQUEST);
+ }
+ }
+
+ private static class MapNamespaceContext implements NamespaceContext {
+ Map<String, String> map;
+
+ private MapNamespaceContext(Map<String, String> map) {
+ this.map = map;
+ }
+
+ @Override
+ public String getNamespaceURI(String s) {
+ return map.get(s);
+ }
+
+ @Override
+ public String getPrefix(String s) {
+ return map.entrySet().stream().filter(e -> e.getValue().equals(s)).findFirst().map(Map.Entry::getKey).orElse(null);
+ }
+
+ @Override
+ public Iterator<String> getPrefixes(String s) {
+ return map.keySet().iterator();
+ }
+ }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
index 291b74e..fa56d4f 100644
--- a/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ServiceValidateEndpoint.java
@@ -27,19 +27,7 @@
@Override
protected Response successResponse() {
UserSessionModel userSession = clientSession.getUserSession();
- // CAS protocol does not support scopes, so pass null scopeParam
- ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, null);
-
- Set<ProtocolMapperModel> mappings = clientSessionCtx.getProtocolMappers();
- KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
- Map<String, Object> attributes = new HashMap<>();
- for (ProtocolMapperModel mapping : mappings) {
- ProtocolMapper mapper = (ProtocolMapper) sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
- if (mapper instanceof CASAttributeMapper) {
- ((CASAttributeMapper) mapper).setAttribute(attributes, mapping, userSession, session, clientSessionCtx);
- }
- }
-
+ Map<String, Object> attributes = getUserAttributes();
CASServiceResponse serviceResponse = ServiceResponseHelper.createSuccess(userSession.getUser().getUsername(), attributes);
return prepare(Response.Status.OK, serviceResponse);
}
diff --git a/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java b/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
index 9e84f0c..e83af7c 100644
--- a/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
+++ b/src/main/java/org/keycloak/protocol/cas/endpoints/ValidateEndpoint.java
@@ -1,50 +1,24 @@
package org.keycloak.protocol.cas.endpoints;
-import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
-import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.common.ClientConnection;
-import org.keycloak.events.Details;
-import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.models.*;
+import org.keycloak.models.RealmModel;
import org.keycloak.protocol.cas.CASLoginProtocol;
-import org.keycloak.protocol.cas.representations.CASErrorCode;
import org.keycloak.protocol.cas.utils.CASValidationException;
-import org.keycloak.protocol.oidc.utils.RedirectUtils;
-import org.keycloak.services.managers.AuthenticationManager;
-import org.keycloak.services.managers.ClientSessionCode;
import javax.ws.rs.GET;
-import javax.ws.rs.core.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
-public class ValidateEndpoint {
- protected static final Logger logger = Logger.getLogger(ValidateEndpoint.class);
+public class ValidateEndpoint extends AbstractValidateEndpoint {
private static final String RESPONSE_OK = "yes\n";
private static final String RESPONSE_FAILED = "no\n";
- @Context
- protected KeycloakSession session;
-
- @Context
- protected ClientConnection clientConnection;
-
- @Context
- protected HttpRequest request;
-
- @Context
- protected HttpHeaders headers;
-
- protected RealmModel realm;
- protected EventBuilder event;
- protected ClientModel client;
- protected AuthenticatedClientSessionModel clientSession;
-
public ValidateEndpoint(RealmModel realm, EventBuilder event) {
- this.realm = realm;
- this.event = event;
+ super(realm, event);
}
@GET
@@ -79,116 +53,4 @@
return Response.status(e.getStatus()).entity(RESPONSE_FAILED).type(MediaType.TEXT_PLAIN).build();
}
- private void checkSsl() {
- if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
- throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "HTTPS required", Response.Status.FORBIDDEN);
- }
- }
-
- private void checkRealm() {
- if (!realm.isEnabled()) {
- throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Realm not enabled", Response.Status.FORBIDDEN);
- }
- }
-
- private void checkClient(String service) {
- if (service == null) {
- event.error(Errors.INVALID_REQUEST);
- throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.SERVICE_PARAM, Response.Status.BAD_REQUEST);
- }
-
- client = realm.getClients().stream()
- .filter(c -> CASLoginProtocol.LOGIN_PROTOCOL.equals(c.getProtocol()))
- .filter(c -> RedirectUtils.verifyRedirectUri(session.getContext().getUri(), service, realm, c) != null)
- .findFirst().orElse(null);
- if (client == null) {
- event.error(Errors.CLIENT_NOT_FOUND);
- throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client not found", Response.Status.BAD_REQUEST);
- }
-
- if (!client.isEnabled()) {
- event.error(Errors.CLIENT_DISABLED);
- throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Client disabled", Response.Status.BAD_REQUEST);
- }
-
- event.client(client.getClientId());
-
- session.getContext().setClient(client);
- }
-
- private void checkTicket(String ticket, boolean requireReauth) {
- if (ticket == null) {
- event.error(Errors.INVALID_CODE);
- throw new CASValidationException(CASErrorCode.INVALID_REQUEST, "Missing parameter: " + CASLoginProtocol.TICKET_PARAM, Response.Status.BAD_REQUEST);
- }
- if (!ticket.startsWith(CASLoginProtocol.SERVICE_TICKET_PREFIX)) {
- event.error(Errors.INVALID_CODE);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET_SPEC, "Malformed service ticket", Response.Status.BAD_REQUEST);
- }
-
- String code = ticket.substring(CASLoginProtocol.SERVICE_TICKET_PREFIX.length());
-
- String[] parts = code.split("\\.");
- if (parts.length == 4) {
- event.detail(Details.CODE_ID, parts[2]);
- }
-
- ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, null, session, realm, client, event, AuthenticatedClientSessionModel.class);
- if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
- event.error(Errors.INVALID_CODE);
-
- // Attempt to use same code twice should invalidate existing clientSession
- AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
- if (clientSession != null) {
- clientSession.detachFromUserSession();
- }
-
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code not valid", Response.Status.BAD_REQUEST);
- }
-
- clientSession = parseResult.getClientSession();
-
- if (parseResult.isExpiredToken()) {
- event.error(Errors.EXPIRED_CODE);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Code is expired", Response.Status.BAD_REQUEST);
- }
-
- clientSession.setNote(CASLoginProtocol.SESSION_SERVICE_TICKET, ticket);
-
- if (requireReauth && AuthenticationManager.isSSOAuthentication(clientSession)) {
- event.error(Errors.SESSION_EXPIRED);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Interactive authentication was requested but not performed", Response.Status.BAD_REQUEST);
- }
-
- UserSessionModel userSession = clientSession.getUserSession();
-
- if (userSession == null) {
- event.error(Errors.USER_SESSION_NOT_FOUND);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User session not found", Response.Status.BAD_REQUEST);
- }
-
- UserModel user = userSession.getUser();
- if (user == null) {
- event.error(Errors.USER_NOT_FOUND);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User not found", Response.Status.BAD_REQUEST);
- }
- if (!user.isEnabled()) {
- event.error(Errors.USER_DISABLED);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "User disabled", Response.Status.BAD_REQUEST);
- }
-
- event.user(userSession.getUser());
- event.session(userSession.getId());
-
- if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
- event.error(Errors.INVALID_CODE);
- throw new CASValidationException(CASErrorCode.INVALID_SERVICE, "Auth error", Response.Status.BAD_REQUEST);
- }
-
- if (!AuthenticationManager.isSessionValid(realm, userSession)) {
- event.error(Errors.USER_SESSION_NOT_FOUND);
- throw new CASValidationException(CASErrorCode.INVALID_TICKET, "Session not active", Response.Status.BAD_REQUEST);
- }
-
- }
}
diff --git a/src/main/java/org/keycloak/protocol/cas/representations/SamlResponseHelper.java b/src/main/java/org/keycloak/protocol/cas/representations/SamlResponseHelper.java
new file mode 100644
index 0000000..f5db51c
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/representations/SamlResponseHelper.java
@@ -0,0 +1,208 @@
+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 javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+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;
+
+public class SamlResponseHelper {
+ private final static DatatypeFactory factory;
+
+ static {
+ try {
+ factory = DatatypeFactory.newInstance();
+ } catch (DatatypeConfigurationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static SAML11ResponseType errorResponse(CASValidationException ex) {
+ 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")));
+ status.setStatusMessage(ex.getErrorDescription());
+ }));
+ });
+ }
+
+ public static SAML11ResponseType successResponse(String issuer, String username, Map<String, Object> attributes) {
+ 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(SAML11StatusCodeType.SUCCESS)));
+ obj.add(applyTo(new SAML11AssertionType("_" + UUID.randomUUID().toString(), now), assertion -> {
+ assertion.setIssuer(issuer);
+ assertion.setConditions(applyTo(new SAML11ConditionsType(), conditions -> {
+ conditions.setNotBefore(now);
+ conditions.setNotOnOrAfter(factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned.plusMinutes(5))));
+ }));
+ assertion.add(applyTo(new SAML11AuthenticationStatementType(
+ URI.create(SAML11Constants.AUTH_METHOD_PASSWORD),
+ now
+ ), stmt -> stmt.setSubject(toSubject(username))));
+ assertion.addAllStatements(toAttributes(username, attributes));
+ }));
+ }
+ );
+ }
+
+ private static List<SAML11StatementAbstractType> toAttributes(String username, Map<String, Object> attributes) {
+ List<SAML11AttributeType> converted = attributeElements(attributes);
+ if (converted.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return Collections.singletonList(applyTo(
+ new SAML11AttributeStatementType(),
+ attrs -> {
+ attrs.setSubject(toSubject(username));
+ attrs.addAllAttributes(converted);
+ })
+ );
+ }
+
+ private static List<SAML11AttributeType> 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) {
+ if (name == null || value == null) {
+ return Stream.empty();
+ }
+
+ if (value instanceof Collection) {
+ return Stream.of(samlAttribute(name, listString((Collection<?>) value)));
+ }
+ return Stream.of(samlAttribute(name, Collections.singletonList(value.toString())));
+ }
+
+ private static SAML11AttributeType samlAttribute(String name, List<Object> listString) {
+ return applyTo(
+ new SAML11AttributeType(name, URI.create("http://www.ja-sig.org/products/cas/")),
+ attr -> attr.addAll(listString)
+ );
+ }
+
+ private static List<Object> listString(Collection<?> value) {
+ return value.stream().map(Object::toString).collect(Collectors.toList());
+ }
+
+ private static SAML11SubjectType toSubject(String username) {
+ return applyTo(
+ new SAML11SubjectType(),
+ subject -> subject.setChoice(
+ new SAML11SubjectType.SAML11SubjectTypeChoice(
+ applyTo(
+ new SAML11NameIdentifierType(username),
+ ctype -> ctype.setFormat(nameIdFormat(username))
+ )
+ )
+ )
+ );
+ }
+
+ private static URI nameIdFormat(String username) {
+ return URI.create(Validation.isEmailValid(username) ?
+ SAML11Constants.FORMAT_EMAIL_ADDRESS :
+ SAML11Constants.FORMAT_UNSPECIFIED
+ );
+ }
+
+ private static <A> A applyTo(A input, Consumer<A> setter) {
+ setter.accept(input);
+ return input;
+ }
+
+ public static String soap(SAML11ResponseType response) {
+ try {
+ Document result = toDOM(response);
+
+ Document doc = wrapSoap(result.getDocumentElement());
+ return toString(doc);
+
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Document toDOM(SAML11ResponseType response) throws ParserConfigurationException, XMLStreamException, ProcessingException {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+
+ XMLOutputFactory factory = XMLOutputFactory.newFactory();
+
+ Document doc = dbf.newDocumentBuilder().newDocument();
+ DOMResult result = new DOMResult(doc);
+ XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(result);
+ SAML11ResponseWriter writer = new SAML11ResponseWriter(xmlWriter);
+ writer.write(response);
+ return doc;
+ }
+
+ private static Document wrapSoap(Node node) throws ParserConfigurationException {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setNamespaceAware(true);
+ Document doc = dbf.newDocumentBuilder().newDocument();
+
+ Element envelope = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Envelope");
+ envelope.appendChild(doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Header"));
+ Element body = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Body");
+
+ Node imported = doc.importNode(node, true);
+
+ body.appendChild(imported);
+ doc.appendChild(body);
+ envelope.appendChild(body);
+ doc.appendChild(envelope);
+ return doc;
+ }
+
+ public static String toString(Document document) throws TransformerException {
+ // Output the Document
+ TransformerFactory tf = TransformerFactory.newInstance();
+ Transformer t = tf.newTransformer();
+ DOMSource source = new DOMSource(document);
+ StringWriter writer = new StringWriter();
+ StreamResult result = new StreamResult(writer);
+ t.transform(source, result);
+ return writer.toString();
+ }
+}
diff --git a/src/test/java/org/keycloak/protocol/cas/SamlResponseTest.java b/src/test/java/org/keycloak/protocol/cas/SamlResponseTest.java
new file mode 100644
index 0000000..0ac2c1c
--- /dev/null
+++ b/src/test/java/org/keycloak/protocol/cas/SamlResponseTest.java
@@ -0,0 +1,48 @@
+package org.keycloak.protocol.cas;
+
+import org.junit.Test;
+import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
+import org.keycloak.protocol.cas.representations.CASErrorCode;
+import org.keycloak.protocol.cas.representations.SamlResponseHelper;
+import org.keycloak.protocol.cas.utils.CASValidationException;
+import org.w3c.dom.Document;
+
+import javax.ws.rs.core.Response;
+import java.util.Collections;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class SamlResponseTest {
+ @Test
+ public void successResponseIsWrappedInSOAP() {
+ SAML11ResponseType 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"));
+ assertTrue(soapResult.contains("keycloak"));
+ }
+
+ @Test
+ public void failureResponseIsWrappedInSOAP() {
+ SAML11ResponseType 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));
+ String output = SamlResponseHelper.toString(SamlResponseHelper.toDOM(response));
+ Document doc = XMLValidator.parseAndValidate(output, XMLValidator.schemaFromClassPath("oasis-sstc-saml-schema-protocol-1.1.xsd"));
+ assertNotNull(doc);
+ }
+
+ @Test
+ public void validateSchemaResponseSuccess() throws Exception {
+ SAML11ResponseType 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);
+ }
+}
diff --git a/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java b/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java
index b8ebe5a..bed1f00 100644
--- a/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java
+++ b/src/test/java/org/keycloak/protocol/cas/ServiceResponseTest.java
@@ -1,7 +1,6 @@
package org.keycloak.protocol.cas;
import com.jayway.jsonpath.JsonPath;
-import com.sun.xml.bind.v2.util.FatalAdapter;
import org.junit.Test;
import org.keycloak.protocol.cas.representations.CASErrorCode;
import org.keycloak.protocol.cas.representations.CASServiceResponse;
@@ -9,20 +8,14 @@
import org.keycloak.protocol.cas.utils.ServiceResponseMarshaller;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
-import org.xml.sax.helpers.DefaultHandler;
import org.xmlunit.xpath.JAXPXPathEngine;
import org.xmlunit.xpath.XPathEngine;
-import javax.xml.XMLConstants;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.validation.Schema;
-import javax.xml.validation.SchemaFactory;
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
import java.util.*;
import static org.junit.Assert.assertEquals;
+import static org.keycloak.protocol.cas.XMLValidator.parseAndValidate;
+import static org.keycloak.protocol.cas.XMLValidator.schemaFromClassPath;
public class ServiceResponseTest {
private final XPathEngine xpath = new JAXPXPathEngine();
@@ -55,7 +48,7 @@
// Build and validate XML response
String xml = ServiceResponseMarshaller.marshalXml(response);
- Document doc = parseAndValidate(xml);
+ Document doc = parseAndValidate(xml, schemaFromClassPath("cas-response-schema.xsd"));
assertEquals("username", xpath.evaluate("/cas:serviceResponse/cas:authenticationSuccess/cas:user", doc));
int idx = 0;
for (Node node : xpath.selectNodes("/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:list", doc)) {
@@ -88,23 +81,8 @@
// Build and validate XML response
String xml = ServiceResponseMarshaller.marshalXml(response);
- Document doc = parseAndValidate(xml);
+ Document doc = parseAndValidate(xml, schemaFromClassPath("cas-response-schema.xsd"));
assertEquals(CASErrorCode.INVALID_REQUEST.name(), xpath.evaluate("/cas:serviceResponse/cas:authenticationFailure/@code", doc));
assertEquals("Error description", xpath.evaluate("/cas:serviceResponse/cas:authenticationFailure", doc));
- }
-
- /**
- * Parse XML document and validate against CAS schema
- */
- private Document parseAndValidate(String xml) throws Exception {
- Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
- .newSchema(getClass().getResource("cas-response-schema.xsd"));
-
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setSchema(schema);
- factory.setNamespaceAware(true);
- DocumentBuilder builder = factory.newDocumentBuilder();
- builder.setErrorHandler(new FatalAdapter(new DefaultHandler()));
- return builder.parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
}
}
diff --git a/src/test/java/org/keycloak/protocol/cas/XMLValidator.java b/src/test/java/org/keycloak/protocol/cas/XMLValidator.java
new file mode 100644
index 0000000..2a85231
--- /dev/null
+++ b/src/test/java/org/keycloak/protocol/cas/XMLValidator.java
@@ -0,0 +1,35 @@
+package org.keycloak.protocol.cas;
+
+import com.sun.xml.bind.v2.util.FatalAdapter;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.StringReader;
+
+public abstract class XMLValidator {
+ private XMLValidator(){}
+
+ public static Schema schemaFromClassPath(String path) throws SAXException {
+ return SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
+ .newSchema(XMLValidator.class.getResource(path));
+ }
+
+ /**
+ * Parse XML document and validate against CAS schema
+ */
+ public static Document parseAndValidate(String xml, Schema schema) throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setSchema(schema);
+ factory.setNamespaceAware(true);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ builder.setErrorHandler(new FatalAdapter(new DefaultHandler()));
+ return builder.parse(new InputSource(new StringReader(xml)));
+ }
+}
diff --git a/src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-assertion-1.1.xsd b/src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-assertion-1.1.xsd
new file mode 100644
index 0000000..dee3a3e
--- /dev/null
+++ b/src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-assertion-1.1.xsd
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified" attributeFormDefault="unqualified" version="1.1">
+ <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd"/>
+ <annotation>
+ <documentation>
+ Document identifier: oasis-sstc-saml-schema-assertion-1.1
+ Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security
+ Revision history:
+ V1.0 (November, 2002):
+ Initial standard schema.
+ V1.1 (September, 2003):
+ * Note that V1.1 of this schema has the same XML namespace as V1.0.
+ Rebased ID content directly on XML Schema types
+ Added DoNotCacheCondition element and DoNotCacheConditionType
+ </documentation>
+ </annotation>
+ <simpleType name="DecisionType">
+ <restriction base="string">
+ <enumeration value="Permit"/>
+ <enumeration value="Deny"/>
+ <enumeration value="Indeterminate"/>
+ </restriction>
+ </simpleType>
+ <element name="AssertionIDReference" type="NCName"/>
+ <element name="Assertion" type="saml:AssertionType"/>
+ <complexType name="AssertionType">
+ <sequence>
+ <element ref="saml:Conditions" minOccurs="0"/>
+ <element ref="saml:Advice" minOccurs="0"/>
+ <choice maxOccurs="unbounded">
+ <element ref="saml:Statement"/>
+ <element ref="saml:SubjectStatement"/>
+ <element ref="saml:AuthenticationStatement"/>
+ <element ref="saml:AuthorizationDecisionStatement"/>
+ <element ref="saml:AttributeStatement"/>
+ </choice>
+ <element ref="ds:Signature" minOccurs="0"/>
+ </sequence>
+ <attribute name="MajorVersion" type="integer" use="required"/>
+ <attribute name="MinorVersion" type="integer" use="required"/>
+ <attribute name="AssertionID" type="ID" use="required"/>
+ <attribute name="Issuer" type="string" use="required"/>
+ <attribute name="IssueInstant" type="dateTime" use="required"/>
+ </complexType>
+ <element name="Conditions" type="saml:ConditionsType"/>
+ <complexType name="ConditionsType">
+ <choice minOccurs="0" maxOccurs="unbounded">
+ <element ref="saml:AudienceRestrictionCondition"/>
+ <element ref="saml:DoNotCacheCondition"/>
+ <element ref="saml:Condition"/>
+ </choice>
+ <attribute name="NotBefore" type="dateTime" use="optional"/>
+ <attribute name="NotOnOrAfter" type="dateTime" use="optional"/>
+ </complexType>
+ <element name="Condition" type="saml:ConditionAbstractType"/>
+ <complexType name="ConditionAbstractType" abstract="true"/>
+ <element name="AudienceRestrictionCondition" type="saml:AudienceRestrictionConditionType"/>
+ <complexType name="AudienceRestrictionConditionType">
+ <complexContent>
+ <extension base="saml:ConditionAbstractType">
+ <sequence>
+ <element ref="saml:Audience" maxOccurs="unbounded"/>
+ </sequence>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="Audience" type="anyURI"/>
+ <element name="DoNotCacheCondition" type="saml:DoNotCacheConditionType"/>
+ <complexType name="DoNotCacheConditionType">
+ <complexContent>
+ <extension base="saml:ConditionAbstractType"/>
+ </complexContent>
+ </complexType>
+ <element name="Advice" type="saml:AdviceType"/>
+ <complexType name="AdviceType">
+ <choice minOccurs="0" maxOccurs="unbounded">
+ <element ref="saml:AssertionIDReference"/>
+ <element ref="saml:Assertion"/>
+ <any namespace="##other" processContents="lax"/>
+ </choice>
+ </complexType>
+ <element name="Statement" type="saml:StatementAbstractType"/>
+ <complexType name="StatementAbstractType" abstract="true"/>
+ <element name="SubjectStatement" type="saml:SubjectStatementAbstractType"/>
+ <complexType name="SubjectStatementAbstractType" abstract="true">
+ <complexContent>
+ <extension base="saml:StatementAbstractType">
+ <sequence>
+ <element ref="saml:Subject"/>
+ </sequence>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="Subject" type="saml:SubjectType"/>
+ <complexType name="SubjectType">
+ <choice>
+ <sequence>
+ <element ref="saml:NameIdentifier"/>
+ <element ref="saml:SubjectConfirmation" minOccurs="0"/>
+ </sequence>
+ <element ref="saml:SubjectConfirmation"/>
+ </choice>
+ </complexType>
+ <element name="NameIdentifier" type="saml:NameIdentifierType"/>
+ <complexType name="NameIdentifierType">
+ <simpleContent>
+ <extension base="string">
+ <attribute name="NameQualifier" type="string" use="optional"/>
+ <attribute name="Format" type="anyURI" use="optional"/>
+ </extension>
+ </simpleContent>
+ </complexType>
+ <element name="SubjectConfirmation" type="saml:SubjectConfirmationType"/>
+ <complexType name="SubjectConfirmationType">
+ <sequence>
+ <element ref="saml:ConfirmationMethod" maxOccurs="unbounded"/>
+ <element ref="saml:SubjectConfirmationData" minOccurs="0"/>
+ <element ref="ds:KeyInfo" minOccurs="0"/>
+ </sequence>
+ </complexType>
+ <element name="SubjectConfirmationData" type="anyType"/>
+ <element name="ConfirmationMethod" type="anyURI"/>
+ <element name="AuthenticationStatement" type="saml:AuthenticationStatementType"/>
+ <complexType name="AuthenticationStatementType">
+ <complexContent>
+ <extension base="saml:SubjectStatementAbstractType">
+ <sequence>
+ <element ref="saml:SubjectLocality" minOccurs="0"/>
+ <element ref="saml:AuthorityBinding" minOccurs="0" maxOccurs="unbounded"/>
+ </sequence>
+ <attribute name="AuthenticationMethod" type="anyURI" use="required"/>
+ <attribute name="AuthenticationInstant" type="dateTime" use="required"/>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="SubjectLocality" type="saml:SubjectLocalityType"/>
+ <complexType name="SubjectLocalityType">
+ <attribute name="IPAddress" type="string" use="optional"/>
+ <attribute name="DNSAddress" type="string" use="optional"/>
+ </complexType>
+ <element name="AuthorityBinding" type="saml:AuthorityBindingType"/>
+ <complexType name="AuthorityBindingType">
+ <attribute name="AuthorityKind" type="QName" use="required"/>
+ <attribute name="Location" type="anyURI" use="required"/>
+ <attribute name="Binding" type="anyURI" use="required"/>
+ </complexType>
+ <element name="AuthorizationDecisionStatement" type="saml:AuthorizationDecisionStatementType"/>
+ <complexType name="AuthorizationDecisionStatementType">
+ <complexContent>
+ <extension base="saml:SubjectStatementAbstractType">
+ <sequence>
+ <element ref="saml:Action" maxOccurs="unbounded"/>
+ <element ref="saml:Evidence" minOccurs="0"/>
+ </sequence>
+ <attribute name="Resource" type="anyURI" use="required"/>
+ <attribute name="Decision" type="saml:DecisionType" use="required"/>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="Action" type="saml:ActionType"/>
+ <complexType name="ActionType">
+ <simpleContent>
+ <extension base="string">
+ <attribute name="Namespace" type="anyURI"/>
+ </extension>
+ </simpleContent>
+ </complexType>
+ <element name="Evidence" type="saml:EvidenceType"/>
+ <complexType name="EvidenceType">
+ <choice maxOccurs="unbounded">
+ <element ref="saml:AssertionIDReference"/>
+ <element ref="saml:Assertion"/>
+ </choice>
+ </complexType>
+ <element name="AttributeStatement" type="saml:AttributeStatementType"/>
+ <complexType name="AttributeStatementType">
+ <complexContent>
+ <extension base="saml:SubjectStatementAbstractType">
+ <sequence>
+ <element ref="saml:Attribute" maxOccurs="unbounded"/>
+ </sequence>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="AttributeDesignator" type="saml:AttributeDesignatorType"/>
+ <complexType name="AttributeDesignatorType">
+ <attribute name="AttributeName" type="string" use="required"/>
+ <attribute name="AttributeNamespace" type="anyURI" use="required"/>
+ </complexType>
+ <element name="Attribute" type="saml:AttributeType"/>
+ <complexType name="AttributeType">
+ <complexContent>
+ <extension base="saml:AttributeDesignatorType">
+ <sequence>
+ <element ref="saml:AttributeValue" maxOccurs="unbounded"/>
+ </sequence>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="AttributeValue" type="anyType"/>
+</schema>
diff --git a/src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-protocol-1.1.xsd b/src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-protocol-1.1.xsd
new file mode 100644
index 0000000..8bea3a9
--- /dev/null
+++ b/src/test/resources/org/keycloak/protocol/cas/oasis-sstc-saml-schema-protocol-1.1.xsd
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<schema targetNamespace="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified" attributeFormDefault="unqualified" version="1.1">
+ <import namespace="urn:oasis:names:tc:SAML:1.0:assertion" schemaLocation="oasis-sstc-saml-schema-assertion-1.1.xsd"/>
+ <import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd"/>
+ <annotation>
+ <documentation>
+ Document identifier: oasis-sstc-saml-schema-protocol-1.1
+ Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security
+ Revision history:
+ V1.0 (November, 2002):
+ Initial standard schema.
+ V1.1 (September, 2003):
+ * Note that V1.1 of this schema has the same XML namespace as V1.0.
+ Rebased ID content directly on XML Schema types
+ </documentation>
+ </annotation>
+ <complexType name="RequestAbstractType" abstract="true">
+ <sequence>
+ <element ref="samlp:RespondWith" minOccurs="0" maxOccurs="unbounded"/>
+ <element ref="ds:Signature" minOccurs="0"/>
+ </sequence>
+ <attribute name="RequestID" type="ID" use="required"/>
+ <attribute name="MajorVersion" type="integer" use="required"/>
+ <attribute name="MinorVersion" type="integer" use="required"/>
+ <attribute name="IssueInstant" type="dateTime" use="required"/>
+ </complexType>
+ <element name="RespondWith" type="QName"/>
+ <element name="Request" type="samlp:RequestType"/>
+ <complexType name="RequestType">
+ <complexContent>
+ <extension base="samlp:RequestAbstractType">
+ <choice>
+ <element ref="samlp:Query"/>
+ <element ref="samlp:SubjectQuery"/>
+ <element ref="samlp:AuthenticationQuery"/>
+ <element ref="samlp:AttributeQuery"/>
+ <element ref="samlp:AuthorizationDecisionQuery"/>
+ <element ref="saml:AssertionIDReference" maxOccurs="unbounded"/>
+ <element ref="samlp:AssertionArtifact" maxOccurs="unbounded"/>
+ </choice>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="AssertionArtifact" type="string"/>
+ <element name="Query" type="samlp:QueryAbstractType"/>
+ <complexType name="QueryAbstractType" abstract="true"/>
+ <element name="SubjectQuery" type="samlp:SubjectQueryAbstractType"/>
+ <complexType name="SubjectQueryAbstractType" abstract="true">
+ <complexContent>
+ <extension base="samlp:QueryAbstractType">
+ <sequence>
+ <element ref="saml:Subject"/>
+ </sequence>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="AuthenticationQuery" type="samlp:AuthenticationQueryType"/>
+ <complexType name="AuthenticationQueryType">
+ <complexContent>
+ <extension base="samlp:SubjectQueryAbstractType">
+ <attribute name="AuthenticationMethod" type="anyURI"/>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="AttributeQuery" type="samlp:AttributeQueryType"/>
+ <complexType name="AttributeQueryType">
+ <complexContent>
+ <extension base="samlp:SubjectQueryAbstractType">
+ <sequence>
+ <element ref="saml:AttributeDesignator" minOccurs="0" maxOccurs="unbounded"/>
+ </sequence>
+ <attribute name="Resource" type="anyURI" use="optional"/>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="AuthorizationDecisionQuery" type="samlp:AuthorizationDecisionQueryType"/>
+ <complexType name="AuthorizationDecisionQueryType">
+ <complexContent>
+ <extension base="samlp:SubjectQueryAbstractType">
+ <sequence>
+ <element ref="saml:Action" maxOccurs="unbounded"/>
+ <element ref="saml:Evidence" minOccurs="0"/>
+ </sequence>
+ <attribute name="Resource" type="anyURI" use="required"/>
+ </extension>
+ </complexContent>
+ </complexType>
+ <complexType name="ResponseAbstractType" abstract="true">
+ <sequence>
+ <element ref="ds:Signature" minOccurs="0"/>
+ </sequence>
+ <attribute name="ResponseID" type="ID" use="required"/>
+ <attribute name="InResponseTo" type="NCName" use="optional"/>
+ <attribute name="MajorVersion" type="integer" use="required"/>
+ <attribute name="MinorVersion" type="integer" use="required"/>
+ <attribute name="IssueInstant" type="dateTime" use="required"/>
+ <attribute name="Recipient" type="anyURI" use="optional"/>
+ </complexType>
+ <element name="Response" type="samlp:ResponseType"/>
+ <complexType name="ResponseType">
+ <complexContent>
+ <extension base="samlp:ResponseAbstractType">
+ <sequence>
+ <element ref="samlp:Status"/>
+ <element ref="saml:Assertion" minOccurs="0" maxOccurs="unbounded"/>
+ </sequence>
+ </extension>
+ </complexContent>
+ </complexType>
+ <element name="Status" type="samlp:StatusType"/>
+ <complexType name="StatusType">
+ <sequence>
+ <element ref="samlp:StatusCode"/>
+ <element ref="samlp:StatusMessage" minOccurs="0"/>
+ <element ref="samlp:StatusDetail" minOccurs="0"/>
+ </sequence>
+ </complexType>
+ <element name="StatusCode" type="samlp:StatusCodeType"/>
+ <complexType name="StatusCodeType">
+ <sequence>
+ <element ref="samlp:StatusCode" minOccurs="0"/>
+ </sequence>
+ <attribute name="Value" type="QName" use="required"/>
+ </complexType>
+ <element name="StatusMessage" type="string"/>
+ <element name="StatusDetail" type="samlp:StatusDetailType"/>
+ <complexType name="StatusDetailType">
+ <sequence>
+ <any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+ </sequence>
+ </complexType>
+</schema>
--
Gitblit v1.9.1