-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.clj
191 lines (166 loc) · 5.88 KB
/
auth.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
187
188
189
190
191
(ns monkey.ci.web.auth
"Authentication and authorization functions"
(:require [buddy.auth :as ba]
[buddy.auth
[backends :as bb]
[middleware :as bmw]]
[buddy.core
[codecs :as codecs]
[keys :as bk]
[nonce :as nonce]]
[buddy.sign.jwt :as jwt]
[clojure.tools.logging :as log]
[java-time.api :as jt]
[monkey.ci
[runtime :as rt]
[storage :as st]
[utils :as u]]
[monkey.ci.web.common :as c]
[ring.middleware.params :as rmp]
[ring.util.response :as rur]))
(def kid "master")
(def role-user "user")
(def role-build "build")
(defn user-token
"Creates token contents for an authenticated user"
[user-sid]
{:role role-user
:sub (u/serialize-sid user-sid)})
(defn build-token
"Creates token contents for a build, to be used by a build script."
[build-sid]
{:role role-build
:sub (u/serialize-sid build-sid)})
(defn generate-secret-key
"Generates a random secret key object"
[]
(-> (nonce/random-nonce 32)
(codecs/bytes->hex)))
(defn sign-jwt [payload pk]
(jwt/sign payload pk {:alg :rs256 :header {:kid kid}}))
(def default-token-expiration (jt/days 1))
(defn augment-payload [payload]
;; TODO Make token expiration configurable
(assoc payload
:exp (-> (jt/plus (jt/instant) default-token-expiration)
(jt/to-millis-from-epoch))
;; TODO Make issuer and audiences configurable
:iss "https://app.monkeyci.com"
:aud ["https://api.monkeyci.com"]))
(defn generate-jwt-from-rt
"Generates a JWT from the private key in the runtime"
[rt payload]
(when-let [pk (get-in rt [:jwk :priv])]
(-> payload
(augment-payload)
(sign-jwt pk))))
(defn generate-jwt
"Signs a JWT using the keypair from the request context."
[req payload]
(-> req
(c/req->rt)
(generate-jwt-from-rt payload)))
(defn generate-keypair
"Generates a new RSA keypair"
[]
(-> (doto (java.security.KeyPairGenerator/getInstance "RSA")
(.initialize 2048))
(.generateKeyPair)))
(defn keypair->rt [kp]
{:pub (.getPublic kp)
:priv (.getPrivate kp)})
(defn config->keypair
"Loads private and public keys from the app config, returns a map that can be
used in the context `:jwk`."
[conf]
(let [m {:private-key bk/private-key
:public-key bk/public-key}
loaded-keys (mapv (fn [[k f]]
(when-let [v (get-in conf [:jwk k])]
(f v)))
m)]
(log/debug "Configured JWK:" (:jwk conf))
(when (every? some? loaded-keys)
(zipmap [:priv :pub] loaded-keys))))
(defn make-jwk
"Creates a JWK object from a public key that can be exposed for external
verification."
[pub]
(-> (bk/public-key->jwk pub)
(assoc :kid kid
;; RS256 is currently the only algorithm supported by OCI api gateway
:alg "RS256"
;; Required by oci api gateway
:use "sig")))
(def rt->pub-key (comp :pub :jwk))
(defn jwks
"JWKS endpoint handler"
[req]
(if-let [k (c/from-rt req rt->pub-key)]
(rur/response {:keys [(make-jwk k)]})
(rur/not-found {:message "No JWKS configured"})))
(defn expired?
"Returns true if token has expired"
[{:keys [exp]}]
(not (and exp (pos? (- exp (u/now))))))
(defmulti resolve-token (fn [_ {:keys [role]}] role))
(defmethod resolve-token role-user [{:keys [storage]} {:keys [sub] :as token}]
(when (and (not (expired? token)) sub)
(let [id (u/parse-sid sub)]
(when (= 2 (count id))
(log/debug "Looking up user with id" id)
(some-> (st/find-user storage id)
(update :customers set))))))
(defmethod resolve-token role-build [{:keys [storage]} {:keys [sub] :as token}]
(when-not (expired? token)
(when-let [build (some->> sub
(u/parse-sid)
(st/find-build storage))]
(assoc build :customers #{(:customer-id build)}))))
(defmethod resolve-token :default [rt token]
;; Fallback, for backwards compatibility
nil)
(defn- query-auth-to-bearer
"Middleware that puts the authorization token query param in the authorization header
if no auth header is provided."
[h]
(fn [req]
(let [header (get-in req [:headers "authorization"])
query (get-in req [:query-params "authorization"])]
(cond-> req
(and query (not header))
(assoc-in [:headers "authorization"] (str "Bearer " query))
true h))))
(defn secure-ring-app
"Wraps the ring handler so it verifies the JWT authorization header"
[app rt]
(let [pk (rt->pub-key rt)
backend (bb/jws {:secret pk
:token-name "Bearer"
:options {:alg :rs256}
:authfn (partial resolve-token rt)})]
(-> app
(bmw/wrap-authentication backend)
;; Also check authorization query arg, because in some cases it's not possible
;; to pass it as a header (e.g. server-sent events).
(query-auth-to-bearer)
(rmp/wrap-params))))
(defn- check-authorization!
"Checks if the request identity grants access to the customer specified in
the parameters path."
[req]
(when-let [cid (get-in req [:parameters :path :customer-id])]
(when-not (and (ba/authenticated? req)
(contains? (get-in req [:identity :customers]) cid))
(throw (ex-info "Credentials do not grant access to this customer"
{:type :auth/unauthorized
:customer-id cid})))))
(defn customer-authorization
"Middleware that verifies the identity token to check if the user or build has
access to the given customer."
[h]
(fn [req]
(check-authorization! req)
(h req)))
(defmethod rt/setup-runtime :jwk [conf _]
(config->keypair conf))