This repository has been archived by the owner on Apr 29, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jwt.clj
202 lines (176 loc) · 7.48 KB
/
jwt.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
192
193
194
195
196
197
198
199
200
201
202
(ns oc.lib.jwt
(:require [if-let.core :refer (if-let* when-let*)]
[defun.core :refer (defun defun-)]
[taoensso.timbre :as timbre]
[schema.core :as schema]
[clj-jwt.core :as jwt]
[clj-time.core :as t]
[clj-time.coerce :as tc]
[oc.lib.db.common :as db-common]
[oc.lib.schema :as lib-schema]))
(def media-type "application/jwt")
(def SlackBots {lib-schema/UniqueID [{:id schema/Str :token schema/Str :slack-org-id schema/Str}]})
(def GoogleToken
{:access-token schema/Str
:token-type schema/Str
:query-param schema/Any
:params {:expires_in schema/Any
:id_token schema/Str
:scope schema/Any}})
(def Claims
(merge {:user-id lib-schema/UniqueID
:teams [lib-schema/UniqueID]
:admin [lib-schema/UniqueID]
:name schema/Str
:first-name schema/Str
:last-name schema/Str
:avatar-url (schema/maybe schema/Str)
:email lib-schema/NonBlankStr
:auth-source schema/Any
(schema/optional-key :slack-id) schema/Str
(schema/optional-key :slack-display-name) schema/Str
(schema/optional-key :slack-token) schema/Str
(schema/optional-key :slack-bots) SlackBots
(schema/optional-key :google-id) schema/Str
(schema/optional-key :google-token) schema/Any
:refresh-url lib-schema/NonBlankStr
:expire schema/Num
schema/Keyword schema/Any} ; and whatever else is in the JWT map to make it open for future extensions
lib-schema/slack-users))
(schema/defn ^:always-validate admin-of :- (schema/maybe [lib-schema/UniqueID])
"
Given the user-id of the user, return a sequence of team-ids for the teams the user is an admin of.
Requires a conn to the auth DB.
"
[conn user-id :- lib-schema/UniqueID]
{:pre [(db-common/conn? conn)]}
(let [teams (db-common/read-resources conn :teams :admins user-id)]
(vec (map :team-id teams))))
(defn name-for
"Fn moved to lib-schema ns. Here for backwards compatability."
([user] (lib-schema/name-for user))
([first last] (lib-schema/name-for first last)))
(defun- bot-for
"
Given a Slack org resource, return the bot properties suitable for use in a JWToken, or nil if there's no bot
for the Slack org.
Or, given a map of Slack orgs to their bots, and a sequence of Slack orgs, return a sequence of bots.
"
;; Single Slack org case
([slack-org]
(when (and (:bot-user-id slack-org) (:bot-token slack-org))
;; Extract and rename the keys for JWToken use
(select-keys
(clojure.set/rename-keys slack-org {:bot-user-id :id :bot-token :token})
[:slack-org-id :id :token])))
;; Empty case, no more Slack orgs
([_bots _slack-orgs :guard empty? results :guard empty?] nil)
([_bots _slack-orgs :guard empty? results] (remove nil? results))
;; Many Slack orgs case, recursively get the bot for each org one by one
([bots slack-orgs results]
(bot-for bots (rest slack-orgs) (conj results (get bots (first slack-orgs))))))
(defun bots-for
"
Given a user, return a map of configured bots for each of the user's teams, keyed by team-id.
Requires a conn to the auth DB.
"
([conn user :guard #(empty? (:teams %))] [])
([conn user]
(let [team-ids (:teams user)
teams (db-common/read-resources-by-primary-keys conn :teams team-ids [:team-id :name :slack-orgs]) ; teams the user is a member of
teams-with-slack (remove #(empty? (:slack-orgs %)) teams) ; teams with a Slack org
slack-org-ids (distinct (flatten (map :slack-orgs teams-with-slack))) ; distinct Slack orgs
slack-orgs (if (empty? slack-org-ids)
[]
;; bot lookup
(db-common/read-resources-by-primary-keys conn :slack_orgs slack-org-ids
[:slack-org-id :name :bot-user-id :bot-token]))
bots (remove nil? (map bot-for slack-orgs)) ; remove slack orgs with no bots
slack-org-to-bot (zipmap (map :slack-org-id bots) bots) ; map of slack org to its bot
team-to-slack-orgs (zipmap (map :team-id teams-with-slack)
(map :slack-orgs teams-with-slack)) ; map of team to its Slack org(s)
team-to-bots (zipmap (keys team-to-slack-orgs)
(map #(bot-for slack-org-to-bot % []) (vals team-to-slack-orgs)))] ; map of team to bot(s)
(into {} (remove (comp empty? second) team-to-bots))))) ; remove any team with no bots
(defn expired?
"Return true/false if the JWToken is expired."
[jwt-claims]
(if-let [expire (:expire jwt-claims)]
(t/after? (t/now) (tc/from-long expire))
(timbre/error "No expire field found in JWToken" jwt-claims)))
(defn expire
"Set an expire property in the JWToken payload, longer if there's a bot, shorter if not."
[payload]
(let [expire-by (-> (if (empty? (:slack-bots payload)) 2 24)
t/hours t/from-now .getMillis)]
(assoc payload :expire expire-by)))
(defn encode [payload passphrase]
(-> payload
jwt/jwt
(jwt/sign :HS256 passphrase)
jwt/to-str))
(defn generate-id-token [claims passphrase]
(encode {:id-token true
:secure-uuid (:secure-uuid claims)
:org-id (:org-uuid claims)
:name (:name claims)
:first-name (:first-name claims)
:last-name (:last-name claims)
:user-id (:user-id claims)
:avatar-url (:avatar-url claims)
:teams [(:team-id claims)]}
passphrase))
(defn generate
"Create a JSON Web Token from a payload."
[payload passphrase]
(let [expiring-payload (expire payload)]
(when-not (:super-user expiring-payload) ;; trust the super user
(schema/validate Claims expiring-payload)) ; ensure we only generate valid JWTokens
(encode expiring-payload passphrase)))
(defn check-token
"Verify a JSON Web Token with the passphrase that was (presumably) used to generate it."
[token passphrase]
(try
(-> token
jwt/str->jwt
(jwt/verify passphrase))
(catch Exception e
false)))
(defn decode
"Decode a JSON Web Token"
[token]
(jwt/str->jwt token))
(defn valid?
[token passphrase]
(try
(if-let* [check? (check-token token passphrase)
claims (:claims (decode token))
expired? (not (expired? claims))]
(do (schema/validate Claims claims)
true)
false)
(catch Exception e
false)))
(defn decode-id-token
"
Decode the id-token.
The first version of the id-token had :team-id key instead of :teams and it was released on production for digest only.
To avoid breaking those links let's move :team-id into :teams (as list) when the id-token is being decoded.
"
[token passphrase]
(when (check-token token passphrase)
(let [decoded-token (decode token)
claims (:claims decoded-token)]
(if (contains? claims :teams)
decoded-token
(assoc-in decoded-token [:claims :teams] [(:team-id claims)])))))
;; Sign/unsign terminology coming from `buddy-sign` project
;; which this namespace should eventually be switched to
;; https://funcool.github.io/buddy-sign/latest/
(defprotocol ITokenSigner
(-sign [this payload] "Generate JWT for given payload")
(-unsign [this token] "Decode a given JWT, nil if not verifiable or otherwise broken"))
(defrecord TokenSigner [passphrase]
ITokenSigner
(-sign [this payload] (generate payload passphrase))
(-unsign [this token] (when (check-token token passphrase) (decode token))))