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