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; |
e1b962
|
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 { |
e1b962
|
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 |
} |