forked from k2n/saml20-clj
-
Notifications
You must be signed in to change notification settings - Fork 11
/
request.clj
145 lines (134 loc) · 6.61 KB
/
request.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
(ns saml20-clj.sp.request
(:require [clojure.string :as str]
[java-time.api :as t]
[ring.util.codec :as codec]
[saml20-clj.coerce :as coerce]
[saml20-clj.crypto :as crypto]
[saml20-clj.encode-decode :as encode-decode]
[saml20-clj.state :as state]))
(defn- format-instant
"Converts a date-time to a SAML 2.0 time string."
[instant]
(t/format (t/format "YYYY-MM-dd'T'HH:mm:ss'Z'" (t/offset-date-time instant (t/zone-offset 0)))))
(defn- non-blank-string? [s]
(and (string? s)
(not (str/blank? s))))
(defn random-request-id
"Generates a random ID for a SAML request, if none is provided."
[]
(str "id" (random-uuid)))
(defn- make-auth-xml [request-id instant sp-name idp-url acs-url issuer]
[:samlp:AuthnRequest
{:xmlns:samlp "urn:oasis:names:tc:SAML:2.0:protocol"
:ID (or request-id (random-request-id))
:Version "2.0"
:IssueInstant (format-instant instant)
:ProtocolBinding "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
:ProviderName sp-name
:IsPassive false
:Destination idp-url
:AssertionConsumerServiceURL acs-url}
[:saml:Issuer
{:xmlns:saml "urn:oasis:names:tc:SAML:2.0:assertion"}
issuer]
;;[:samlp:NameIDPolicy {:AllowCreate false :Format saml-format}]
])
(defn request
"Return XML elements that represent a SAML 2.0 auth request."
^org.w3c.dom.Element [{:keys [ ;; e.g. something like a UUID. Random UUID will be used if no other ID is provided
request-id
;; e.g. "Metabase"
sp-name
;; e.g. http://sp.example.com/demo1/index.php?acs
acs-url
;; e.g. http://idp.example.com/SSOService.php
idp-url
;; e.g. http://sp.example.com/demo1/metadata.php
issuer
;; If present, record the request
state-manager
;; If present, we can sign the request
credential
instant]
:or {instant (t/instant)}}]
(assert (non-blank-string? acs-url) "acs-url is required")
(assert (non-blank-string? idp-url) "idp-url is required")
(assert (non-blank-string? sp-name) "sp-name is required")
(assert (non-blank-string? issuer) "issuer is required")
(let [request (coerce/->Element
(coerce/->xml-string
(make-auth-xml request-id instant sp-name idp-url acs-url issuer)))]
(when state-manager
(state/record-request! state-manager (.getAttribute request "ID")))
(if-not credential
request
(or (crypto/sign request credential)
(throw (ex-info "Failed to sign request" {:request request}))))))
(defn- add-query-params
"Add query parameters to a URL.
(add-query-params \"http://example.com\" {:a \"b\" :c \"d\"}
;; => \"http://example.com?a=b&c=d\""
[url params]
(str url (if (str/includes? url "?") "&" "?") (codec/form-encode params)))
(defn idp-redirect-response
"Return Ring response for HTTP 302 redirect."
[saml-request idp-url relay-state]
{:pre [(some? saml-request)
(string? idp-url)
(string? relay-state)]}
(let [saml-request-str (cond-> saml-request
(not (string? saml-request)) coerce/->xml-string)
saml-request-str (encode-decode/str->deflate->base64 saml-request-str)
url (add-query-params idp-url {:SAMLRequest saml-request-str
:RelayState relay-state})]
{:status 302 ; found
:headers {"Location" url}
:body ""}))
;; I wanted to call this make-request-xml, but it gets exported in core.clj, which
;; warrants the request prefix
(defn make-logout-request-xml
"Generates a SAML 2.0 logout request, as a hiccupey datastructure."
[& {:keys [request-id instant idp-url issuer user-email]
:or {instant (format-instant (t/instant))}}]
(assert (non-blank-string? idp-url) "idp-url is required")
(assert (non-blank-string? issuer) "issuer is required")
(assert (non-blank-string? user-email) "user-email is required")
[:samlp:LogoutRequest {:xmlns:samlp "urn:oasis:names:tc:SAML:2.0:protocol"
:xmlns:saml "urn:oasis:names:tc:SAML:2.0:assertion"
:Version "2.0"
:ID (or request-id (str "id" (random-uuid)))
:IssueInstant instant
:Destination idp-url}
[:saml:Issuer issuer]
[:saml:NameID {:Format "urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"} user-email]
[:samlp:SessionIndex "SessionIndex_From_Authentication_Assertion"]])
(defn logout-redirect-location
"This returns a url that you'd want to redirect a client to. Either using
`ring/redirect` with a 302 status code or passing it to a client in a post body
to have them redirect to."
[& {:keys [issuer user-email idp-url relay-state request-id]}]
(assert (non-blank-string? idp-url) "idp-url is required")
(assert (non-blank-string? user-email) "user-email is required")
(assert (non-blank-string? issuer) "issuer is required")
(assert (non-blank-string? relay-state) "relay-state is required")
(add-query-params idp-url {:SAMLRequest (encode-decode/str->deflate->base64
(coerce/->xml-string (make-logout-request-xml
:idp-url idp-url
:request-id request-id
:issuer issuer
:user-email user-email)))
:RelayState relay-state}))
(defn idp-logout-redirect-response
"Return Ring response for HTTP 302 redirect."
([issuer user-email idp-url relay-state]
(idp-logout-redirect-response issuer user-email idp-url relay-state (random-request-id)))
([issuer user-email idp-url relay-state request-id]
(let [url (logout-redirect-location
:idp-url idp-url
:user-email user-email
:issuer issuer
:relay-state relay-state
:request-id request-id)]
{:status 302 ; found
:headers {"Location" url}
:body ""})))