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

Jacek Kowalski
2023-07-12 fdb9f6bf5fc43d54c9396dc4dd577b6c84ecdb9d
commit | author | age
74023a 1 package org.keycloak.protocol.cas.endpoints;
EH 2
fdb9f6 3 import jakarta.ws.rs.Consumes;
JK 4 import jakarta.ws.rs.POST;
5 import jakarta.ws.rs.Produces;
6 import jakarta.ws.rs.core.MultivaluedMap;
7 import jakarta.ws.rs.core.Response;
74023a 8 import org.keycloak.dom.saml.v1.protocol.SAML11ResponseType;
EH 9 import org.keycloak.events.EventBuilder;
10 import org.keycloak.events.EventType;
11 import org.keycloak.models.RealmModel;
12 import org.keycloak.models.UserModel;
13 import org.keycloak.protocol.cas.CASLoginProtocol;
14 import org.keycloak.protocol.cas.representations.CASErrorCode;
15 import org.keycloak.protocol.cas.representations.SamlResponseHelper;
16 import org.keycloak.protocol.cas.utils.CASValidationException;
17 import org.keycloak.services.Urls;
18 import org.xml.sax.InputSource;
19
20 import javax.xml.namespace.NamespaceContext;
21 import javax.xml.xpath.XPath;
22 import javax.xml.xpath.XPathExpression;
23 import javax.xml.xpath.XPathExpressionException;
24 import javax.xml.xpath.XPathFactory;
25 import java.io.StringReader;
fdb9f6 26 import java.util.Collections;
JK 27 import java.util.Iterator;
28 import java.util.Map;
29 import java.util.Optional;
74023a 30
EH 31 import static org.keycloak.protocol.cas.CASLoginProtocol.TARGET_PARAM;
32
33 public class SamlValidateEndpoint extends AbstractValidateEndpoint {
34     public SamlValidateEndpoint(RealmModel realm, EventBuilder event) {
35         super(realm, event.event(EventType.CODE_TO_TOKEN));
36     }
37
38     @POST
39     @Consumes("text/xml;charset=utf-8")
40     @Produces("text/xml;charset=utf-8")
41     public Response validate(String input) {
4a0a62 42         MultivaluedMap<String, String> queryParams = session.getContext().getUri().getQueryParameters();
74023a 43         try {
4a0a62 44             String soapAction = Optional.ofNullable(session.getContext().getRequestHeaders().getHeaderString("SOAPAction")).map(s -> s.trim().replace("\"", "")).orElse("");
74023a 45             if (!soapAction.equals("http://www.oasis-open.org/committees/security")) {
EH 46                 throw new CASValidationException(CASErrorCode.INTERNAL_ERROR, "Not a validation request", Response.Status.BAD_REQUEST);
47             }
48
49             String service = queryParams.getFirst(TARGET_PARAM);
50             boolean renew = queryParams.containsKey(CASLoginProtocol.RENEW_PARAM);
51
52             checkRealm();
53             checkSsl();
54             checkClient(service);
4a0a62 55             String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName());
74023a 56             String ticket = getTicket(input);
EH 57
58             checkTicket(ticket, renew);
59             UserModel user = clientSession.getUserSession().getUser();
60
61             Map<String, Object> attributes = getUserAttributes();
62
63             SAML11ResponseType response = SamlResponseHelper.successResponse(issuer, user.getUsername(), attributes);
64
65             return Response.ok(SamlResponseHelper.soap(response)).build();
66
67         } catch (CASValidationException ex) {
68             logger.warnf("Invalid SAML1.1 token %s", ex.getErrorDescription());
69
70             SAML11ResponseType response = SamlResponseHelper.errorResponse(ex);
71             return Response.ok().entity(SamlResponseHelper.soap(response)).build();
72         }
73     }
74
75     private String getTicket(String input) {
76         try {
77             XPath xPath = XPathFactory.newInstance().newXPath();
78             xPath.setNamespaceContext(new MapNamespaceContext(Collections.singletonMap("samlp", "urn:oasis:names:tc:SAML:1.0:protocol")));
79
80             XPathExpression expression = xPath.compile("//samlp:AssertionArtifact/text()");
81
82             return expression.evaluate(new InputSource(new StringReader(input)));
83         } catch (XPathExpressionException ex) {
84             throw new CASValidationException(CASErrorCode.INVALID_TICKET, ex.getMessage(), Response.Status.BAD_REQUEST);
85         }
86     }
87
88     private static class MapNamespaceContext implements NamespaceContext {
89         Map<String, String> map;
90
91         private MapNamespaceContext(Map<String, String> map) {
92             this.map = map;
93         }
94
95         @Override
96         public String getNamespaceURI(String s) {
97             return map.get(s);
98         }
99
100         @Override
101         public String getPrefix(String s) {
102             return map.entrySet().stream().filter(e -> e.getValue().equals(s)).findFirst().map(Map.Entry::getKey).orElse(null);
103         }
104
105         @Override
106         public Iterator<String> getPrefixes(String s) {
107             return map.keySet().iterator();
108         }
109     }
110 }