mirror of https://github.com/jacekkow/keycloak-protocol-cas

Jacek Kowalski
2023-06-28 bba8bfec21d26bc06fe25a3b2ecb827b80fc0fb1
commit | author | age
74023a 1 package org.keycloak.protocol.cas.representations;
EH 2
3 import org.keycloak.dom.saml.v1.assertion.*;
4 import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
5 import org.keycloak.dom.saml.v1.protocol.SAML11StatusCodeType;
6 import org.keycloak.dom.saml.v1.protocol.SAML11StatusType;
7 import org.keycloak.protocol.cas.utils.CASValidationException;
8 import org.keycloak.saml.common.exceptions.ProcessingException;
9 import org.keycloak.saml.processing.core.saml.v1.SAML11Constants;
10 import org.keycloak.saml.processing.core.saml.v1.writers.SAML11ResponseWriter;
11 import org.keycloak.services.validation.Validation;
12 import org.w3c.dom.Document;
13 import org.w3c.dom.Element;
14 import org.w3c.dom.Node;
15
16 import javax.xml.datatype.DatatypeConfigurationException;
17 import javax.xml.datatype.DatatypeFactory;
18 import javax.xml.datatype.XMLGregorianCalendar;
19 import javax.xml.namespace.QName;
20 import javax.xml.parsers.DocumentBuilderFactory;
21 import javax.xml.parsers.ParserConfigurationException;
22 import javax.xml.stream.XMLOutputFactory;
23 import javax.xml.stream.XMLStreamException;
24 import javax.xml.stream.XMLStreamWriter;
25 import javax.xml.transform.Transformer;
26 import javax.xml.transform.TransformerException;
27 import javax.xml.transform.TransformerFactory;
28 import javax.xml.transform.dom.DOMResult;
29 import javax.xml.transform.dom.DOMSource;
30 import javax.xml.transform.stream.StreamResult;
31 import java.io.StringWriter;
32 import java.net.URI;
33 import java.time.ZoneOffset;
34 import java.time.ZonedDateTime;
35 import java.util.*;
36 import java.util.function.Consumer;
37 import java.util.stream.Collectors;
38 import java.util.stream.Stream;
39
40 public class SamlResponseHelper {
41     private final static DatatypeFactory factory;
42
43     static {
44         try {
45             factory = DatatypeFactory.newInstance();
46         } catch (DatatypeConfigurationException e) {
47             throw new RuntimeException(e);
48         }
49     }
50
51     public static SAML11ResponseType errorResponse(CASValidationException ex) {
52         ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC);
53         XMLGregorianCalendar now = factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned));
54
55         return applyTo(new SAML11ResponseType("_" + UUID.randomUUID().toString(), now), obj -> {
56             obj.setStatus(applyTo(new SAML11StatusType(), status -> {
57                 status.setStatusCode(new SAML11StatusCodeType(QName.valueOf("samlp:RequestDenied")));
58                 status.setStatusMessage(ex.getErrorDescription());
59             }));
60         });
61     }
62
63     public static SAML11ResponseType successResponse(String issuer, String username, Map<String, Object> attributes) {
64         ZonedDateTime nowZoned = ZonedDateTime.now(ZoneOffset.UTC);
65         XMLGregorianCalendar now = factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned));
66
67         return applyTo(new SAML11ResponseType("_" + UUID.randomUUID().toString(), now),
68                 obj -> {
69                     obj.setStatus(applyTo(new SAML11StatusType(), status -> status.setStatusCode(SAML11StatusCodeType.SUCCESS)));
70                     obj.add(applyTo(new SAML11AssertionType("_" + UUID.randomUUID().toString(), now), assertion -> {
71                         assertion.setIssuer(issuer);
72                         assertion.setConditions(applyTo(new SAML11ConditionsType(), conditions -> {
73                             conditions.setNotBefore(now);
74                             conditions.setNotOnOrAfter(factory.newXMLGregorianCalendar(GregorianCalendar.from(nowZoned.plusMinutes(5))));
75                         }));
76                         assertion.add(applyTo(new SAML11AuthenticationStatementType(
8379a3 77                                 URI.create(SAMLCASConstants.AUTH_METHOD_PASSWORD),
74023a 78                                 now
EH 79                         ), stmt -> stmt.setSubject(toSubject(username))));
80                         assertion.addAllStatements(toAttributes(username, attributes));
81                     }));
82                 }
83         );
84     }
85
86     private static List<SAML11StatementAbstractType> toAttributes(String username, Map<String, Object> attributes) {
87         List<SAML11AttributeType> converted = attributeElements(attributes);
88         if (converted.isEmpty()) {
89             return Collections.emptyList();
90         }
91         return Collections.singletonList(applyTo(
92                 new SAML11AttributeStatementType(),
93                 attrs -> {
94                     attrs.setSubject(toSubject(username));
95                     attrs.addAllAttributes(converted);
96                 })
97         );
98     }
99
100     private static List<SAML11AttributeType> attributeElements(Map<String, Object> attributes) {
101         return attributes.entrySet().stream().flatMap(e ->
102                 toAttribute(e.getKey(), e.getValue())
103         ).filter(a -> !a.get().isEmpty()).collect(Collectors.toList());
104     }
105
106     private static Stream<SAML11AttributeType> toAttribute(String name, Object value) {
107         if (name == null || value == null) {
108             return Stream.empty();
109         }
110
111         if (value instanceof Collection) {
112             return Stream.of(samlAttribute(name, listString((Collection<?>) value)));
113         }
114         return Stream.of(samlAttribute(name, Collections.singletonList(value.toString())));
115     }
116
117     private static SAML11AttributeType samlAttribute(String name, List<Object> listString) {
118         return applyTo(
119                 new SAML11AttributeType(name, URI.create("http://www.ja-sig.org/products/cas/")),
120                 attr -> attr.addAll(listString)
121         );
122     }
123
124     private static List<Object> listString(Collection<?> value) {
125         return value.stream().map(Object::toString).collect(Collectors.toList());
126     }
127
128     private static SAML11SubjectType toSubject(String username) {
129         return applyTo(
130                 new SAML11SubjectType(),
131                 subject -> subject.setChoice(
132                         new SAML11SubjectType.SAML11SubjectTypeChoice(
133                                 applyTo(
134                                         new SAML11NameIdentifierType(username),
135                                         ctype -> ctype.setFormat(nameIdFormat(username))
136                                 )
137                         )
138                 )
139         );
140     }
141
142     private static URI nameIdFormat(String username) {
143         return URI.create(Validation.isEmailValid(username) ?
8379a3 144                 SAMLCASConstants.FORMAT_EMAIL_ADDRESS :
P 145                 SAMLCASConstants.FORMAT_UNSPECIFIED
74023a 146         );
EH 147     }
148
149     private static <A> A applyTo(A input, Consumer<A> setter) {
150         setter.accept(input);
151         return input;
152     }
153
154     public static String soap(SAML11ResponseType response) {
155         try {
156             Document result = toDOM(response);
157
158             Document doc = wrapSoap(result.getDocumentElement());
159             return toString(doc);
160
161         } catch (Exception e) {
162             throw new RuntimeException(e);
163         }
164     }
165
166     public static Document toDOM(SAML11ResponseType response) throws ParserConfigurationException, XMLStreamException, ProcessingException {
167         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
168         dbf.setNamespaceAware(true);
169
170         XMLOutputFactory factory = XMLOutputFactory.newFactory();
171
172         Document doc = dbf.newDocumentBuilder().newDocument();
173         DOMResult result = new DOMResult(doc);
174         XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(result);
175         SAML11ResponseWriter writer = new SAML11ResponseWriter(xmlWriter);
176         writer.write(response);
177         return doc;
178     }
179
180     private static Document wrapSoap(Node node) throws ParserConfigurationException {
181         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
182         dbf.setNamespaceAware(true);
183         Document doc = dbf.newDocumentBuilder().newDocument();
184
185         Element envelope = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Envelope");
186         envelope.appendChild(doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Header"));
187         Element body = doc.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Body");
188
189         Node imported = doc.importNode(node, true);
190
191         body.appendChild(imported);
192         doc.appendChild(body);
193         envelope.appendChild(body);
194         doc.appendChild(envelope);
195         return doc;
196     }
197
198     public static String toString(Document document) throws TransformerException {
199         // Output the Document
200         TransformerFactory tf = TransformerFactory.newInstance();
201         Transformer t = tf.newTransformer();
202         DOMSource source = new DOMSource(document);
203         StringWriter writer = new StringWriter();
204         StreamResult result = new StreamResult(writer);
205         t.transform(source, result);
206         return writer.toString();
207     }
208 }