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

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