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

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