/
token.clj
186 lines (151 loc) · 6.46 KB
/
token.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 cerber.stores.token
(:require [mount.core :refer [defstate]]
[cerber
[db :as db]
[config :refer [app-config]]
[helpers :as helpers]
[store :refer :all]]
[failjure.core :as f]
[cerber.stores.user :as user]
[cerber.error :as error]
[cerber.helpers :as helpers]
[clojure.string :refer [join split]]))
(defn default-valid-for []
(-> app-config :tokens :valid-for))
(declare ->map)
(defrecord Token [client-id user-id login scope secret created-at expires-at])
(defrecord SqlTokenStore []
Store
(fetch-one [this [client-id tag secret login]]
(->map (first (db/find-tokens-by-secret {:secret secret :tag tag}))))
(fetch-all [this [client-id tag secret login]]
(map ->map (if secret
(db/find-tokens-by-secret {:secret secret :tag tag})
(if client-id
(db/find-tokens-by-login-and-client {:client-id client-id :login login :tag tag})
(db/find-tokens-by-login {:login login :tag tag})))))
(revoke-one! [this [client-id tag secret]]
(db/delete-token-by-secret {:secret secret}))
(revoke-all! [this [client-id tag secret login]]
(map ->Token (if login
(db/delete-tokens-by-login {:client-id client-id :login login :tag tag})
(db/delete-tokens-by-client {:client-id client-id :tag tag}))))
(store! [this k token]
(when (= 1 (db/insert-token token)) token))
(purge! [this]
(db/clear-tokens)))
(defmulti create-token-store identity)
(defmacro with-token-store
"Changes default binding to default token store."
[store & body]
`(binding [*token-store* ~store] ~@body))
(defstate ^:dynamic *token-store*
:start (create-token-store (-> app-config :tokens :store))
:stop (helpers/stop-periodic *token-store*))
(defmethod create-token-store :in-memory [_]
(->MemoryStore "tokens" (atom {})))
(defmethod create-token-store :redis [_]
(->RedisStore "tokens" (:redis-spec app-config)))
(defmethod create-token-store :sql [_]
(helpers/with-periodic-fn
(->SqlTokenStore) db/clear-expired-tokens 60000))
(defn create-token
"Creates new token."
[tag client user scope & [ttl]]
(let [secret (helpers/generate-secret)
token (helpers/reset-ttl
{:client-id (:id client)
:user-id (:id user)
:login (:login user)
:secret (helpers/digest secret)
:scope scope
:tag (name tag)
:created-at (helpers/now)}
(and (= tag :access) (or ttl (default-valid-for))))
keyvec (if (= tag :access)
[nil :tag :secret nil]
[:client-id :tag :secret :login])]
(if-let [result (store! *token-store* keyvec token)]
(map->Token (assoc result :secret secret))
(error/internal-error "Cannot create token"))))
;; revocation
(defn- revoke-by-pattern [pattern] (revoke-all! *token-store* pattern) nil)
(defn- revoke-by-key [key] (revoke-one! *token-store* key) nil)
(defn revoke-access-token
[token]
(when-let [secret (:secret token)]
(revoke-by-key [nil "access" (helpers/digest (:secret token)) nil])))
(defn revoke-client-tokens
([client]
(revoke-client-tokens client nil))
([client login]
(revoke-by-pattern [(:id client) "access" nil login])
(revoke-by-pattern [(:id client) "refresh" nil login])))
;; retrieval
(defn find-by-pattern
"Finds token by vectorized pattern key.
Each nil element of key will be replaced with wildcard specific for underlaying store implementation."
[key]
(when-let [tokens (fetch-all *token-store* key)]
(map (fn [t] (map->Token t)) tokens)))
(defn find-by-key
"Finds token by vectorized exact key.
Each element of key is used to compose query depending on underlaying store implementation."
[key]
(when-let [result (fetch-one *token-store* key)]
(map->Token result)))
(defn find-access-token
"Finds access token issued for given client-user pair with particular auto-generated secret code."
[secret]
(find-by-key [nil "access" (helpers/digest secret) nil]))
(defn find-refresh-token
"Finds refresh token issued for given client-user pair with particular auto-generated secret code."
[client-id secret login]
(first (find-by-pattern [client-id "refresh" (helpers/digest secret) login])))
(defn purge-tokens
"Removes token from store. Used for tests only."
[]
(purge! *token-store*))
;; generation
(defn generate-access-token
"Generates access-token for given client-user pair within provided scope.
Additional options (type, refresh?) may adjust token type (Bearer by default)
and decide whether to generate refresh-token as well or not (no refresh-tokens by default).
Asking again for refresh-token generation (through :refresh? true option) reuses prevously
generated refresh-token for given client/user pair."
[client user scope & [opts]]
(let [access-token (and (nil? (revoke-by-pattern [(:id client) "access" nil (:login user)]))
(create-token :access client user scope))
{:keys [client-id secret created-at expires-at login]} access-token
{:keys [type refresh?] :or {type "Bearer"}} opts]
(if (f/failed? access-token)
access-token
(let [refresh-token (and refresh?
(nil? (revoke-by-pattern [client-id "refresh" nil login]))
(create-token :refresh client user scope))]
(-> {:access_token secret
:token_type type
:created_at created-at
:expires_in (quot (- (.getTime expires-at)
(.getTime created-at)) 1000)}
(cond-> scope
(assoc :scope scope))
(cond-> (and refresh-token (not (f/failed? refresh-token)))
(assoc :refresh_token (:secret refresh-token))))))))
(defn refresh-access-token
"Refreshes access and refresh-tokens using provided refresh-token."
[refresh-token]
(let [{:keys [client-id user-id login scope]} refresh-token]
(generate-access-token {:id client-id}
{:id user-id :login login}
scope
{:refresh? true})))
(defn ->map [result]
(when-let [{:keys [client_id user_id login scope secret created_at expires_at]} result]
{:client-id client_id
:user-id user_id
:login login
:scope scope
:secret secret
:created-at created_at
:expires-at expires_at}))