forked from b-social/liberator-mixin
-
Notifications
You must be signed in to change notification settings - Fork 1
/
core.clj
186 lines (161 loc) · 6.04 KB
/
core.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
(ns liberator.mixin.authorisation.core
"Liberator mixin to authorise a request based on an access token"
(:require
[buddy.auth.http :as http]
[buddy.sign.jwt :as jwt]
[clojure.string :as string]
[liberator.representation :as r]))
(defprotocol ClaimValidator
(validate
[this ctx claims]
"Validate a tokens claims.
Params:
* ctx - liberator context
* claims - token claims
Returns an array of:
* valid?
* error map containing message and cause metadata"))
(defn- parse-header
[request token-name token-parser token-header-name]
(cond
(coll? token-name)
(some
#(parse-header request % token-parser token-header-name)
token-name)
(nil? token-name)
(token-parser (http/-get-header request token-header-name))
:else
(let [header (http/-get-header request token-header-name)
cases [(string/capitalize token-name)
(string/lower-case token-name)
(string/upper-case token-name)]
pattern (re-pattern
(str "^(?:" (string/join "|" cases) ") (.+)$"))]
(some->> header
(re-find pattern)
(second)
(token-parser)))))
(defn- is-valid?
[ctx validators claims]
(doseq [validator validators
:let [[valid? error] (validate validator ctx claims)
{:keys [message cause]
:or {message "Access token failed validation."
cause {:type :validation :cause :claims}}} error]]
(when-not (true? valid?) (throw (ex-info message cause))))
true)
(defn- token->identity
[key options token]
(try
(let [claims (jwt/unsign token key options)]
[true {:identity claims}])
(catch Exception e
[false {:www-authenticate {:message (ex-message e)
:error "invalid_token"
:exception e}}])))
(def missing-token
{:www-authenticate {:message "Authorisation header does not contain a token."
:error "invalid_request"}})
(defn with-bearer-token
"Returns a mixin that extracts the access token from the authorisation header
* token-header-name - the name of the header containing the token
(defaults to \"authorization\")
* token-type - the scheme or a list of schemes under the authorisation header
(default is Bearer). Use nil when no type on header.
* token-parser - a function that performs parsing of the token before
validation (optional)
This mixin should only be used once."
[]
{:initialize-context
(fn [{:keys [request resource]}]
(let [token-header-name
(get resource :token-header-name (constantly "authorization"))
token-type
(get resource :token-type (constantly "Bearer"))
token-parser
(get resource :token-parser identity)
token
(parse-header request (token-type) token-parser (token-header-name))]
{:token token}))})
(defn with-token-authorization
"Returns a mixin that validates the jws access token ensure it includes the
claims and that claim passes validation, finally it stores the authentication
and authorisation state on the context under :identity
This mixin assumes a token already on the context under :token
* token-key - the secret can be a function which is provided the JOSE header
as its single param
* token-options - that is used to validate the standard claims of the
token (aud, iss, sub, exp, nbf, iat) (optional)
* token-validators - a array of ClaimValidators (optional)
* token-required? - whether a token should be treated as mandatory (defaults
to true)
This mixin should only be used once."
[]
{:authorized?
(fn [{:keys [token resource request]}]
(let [{:keys [token-required?]
:or {token-required? (constantly {:any true})}} resource
method (:request-method request)
token-required? (token-required?)
token-required? (or (get token-required? method)
(get token-required? :any))
{:keys [token-options token-key]
:or {token-options (constantly {})}} resource]
(cond
(some? token)
(token->identity token-key (token-options) token)
(true? token-required?)
[false missing-token]
:else
true)))
:allowed?
(fn [{:keys [identity resource] :as ctx}]
(if (some? identity)
(let [{:keys [token-validators]
:or {token-validators (constantly [])}} resource]
(try
(is-valid? ctx (token-validators) identity)
(catch Exception e
[false {:www-authenticate
{:message (ex-message e)
:error "insufficient_scope"
:exception e}}])))
true))})
(defn- error->header
[{:keys [error message]}]
(str
"Bearer,\n"
"error=\"" error "\",\n"
"error_message=\"" message "\"\n"))
(defn with-www-authenticate-header
"Returns a mixin that populates the WWW-Authenticate header when the
request is not allowed to access the protected endpoint.
This mixin should only be used once."
[]
{:as-response
(fn [d {:keys [www-authenticate] :as ctx}]
(assoc-in (r/as-response d ctx)
[:headers "WWW-Authenticate"]
(error->header www-authenticate)))})
(defn with-jws-access-token-mixin
[]
[(with-bearer-token)
(with-token-authorization)
(with-www-authenticate-header)])
(deftype ScopeValidator
[required-scopes]
ClaimValidator
(validate [_ ctx claims]
(let [method (get-in ctx [:request :request-method])]
(if-let [required-scopes
(or (get required-scopes method)
(get required-scopes :any))]
(let [scope (:scope claims)]
(if
(and
(some? scope)
(every? (set (string/split scope #" ")) required-scopes))
[true]
[false {:message "Access token failed validation for scope."
:cause {:type :validation :cause :claims}}]))
[true]))))