0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-23 23:18:48 -05:00

♻️ Refactor token generation API

This commit is contained in:
Andrey Antukh 2022-08-24 13:44:33 +02:00
parent 44f4d9c50c
commit d6d9d25fce
16 changed files with 167 additions and 183 deletions

View file

@ -15,9 +15,11 @@
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.client :as http]
[app.http.middleware :as hmw] [app.http.middleware :as hmw]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.util.json :as json] [app.util.json :as json]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
@ -47,7 +49,7 @@
(defn- discover-oidc-config (defn- discover-oidc-config
[{:keys [http-client]} {:keys [base-uri] :as opts}] [{:keys [http-client]} {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration") (let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))] response (ex/try (http/req! http-client {:method :get :uri (str discovery-uri)} {:sync? true}))]
(cond (cond
(ex/exception? response) (ex/exception? response)
(do (do
@ -158,10 +160,10 @@
(defn- retrieve-github-email (defn- retrieve-github-email
[{:keys [http-client]} tdata info] [{:keys [http-client]} tdata info]
(or (some-> info :email p/resolved) (or (some-> info :email p/resolved)
(-> (http-client {:uri "https://api.github.com/user/emails" (-> (http/req! http-client {:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))} :headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000 :timeout 6000
:method :get}) :method :get})
(p/then (fn [{:keys [status body] :as response}] (p/then (fn [{:keys [status body] :as response}]
(when-not (s/int-in-range? 200 300 status) (when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal (ex/raise :type :internal
@ -278,7 +280,7 @@
:uri (:token-uri provider) :uri (:token-uri provider)
:body (u/map->query-string params)}] :body (u/map->query-string params)}]
(p/then (p/then
(http-client req) (http/req! http-client req)
(fn [{:keys [status body] :as res}] (fn [{:keys [status body] :as res}]
(if (= status 200) (if (= status 200)
(let [data (json/read body)] (let [data (json/read body)]
@ -292,11 +294,10 @@
(defn- retrieve-user-info (defn- retrieve-user-info
[{:keys [provider http-client] :as cfg} tdata] [{:keys [provider http-client] :as cfg} tdata]
(letfn [(retrieve [] (letfn [(retrieve []
(http-client {:uri (:user-uri provider) (http/req! http-client {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))} :headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000 :timeout 6000
:method :get})) :method :get}))
(validate-response [response] (validate-response [response]
(when-not (s/int-in-range? 200 300 (:status response)) (when-not (s/int-in-range? 200 300 (:status response))
(ex/raise :type :internal (ex/raise :type :internal
@ -353,7 +354,7 @@
::props])) ::props]))
(defn retrieve-info (defn retrieve-info
[{:keys [tokens provider] :as cfg} {:keys [params] :as request}] [{:keys [sprops provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [info] (letfn [(validate-oidc [info]
;; If the provider is OIDC, we can proceed to check ;; If the provider is OIDC, we can proceed to check
;; roles if they are defined. ;; roles if they are defined.
@ -392,7 +393,7 @@
(let [state (get params :state) (let [state (get params :state)
code (get params :code) code (get params :code)
state (tokens :verify {:token state :iss :oauth})] state (tokens/verify sprops {:token state :iss :oauth})]
(-> (p/resolved code) (-> (p/resolved code)
(p/then #(retrieve-access-token cfg %)) (p/then #(retrieve-access-token cfg %))
(p/then #(retrieve-user-info cfg %)) (p/then #(retrieve-user-info cfg %))
@ -420,13 +421,13 @@
(redirect-response uri))) (redirect-response uri)))
(defn- generate-redirect (defn- generate-redirect
[{:keys [tokens session audit] :as cfg} request info profile] [{:keys [sprops session audit] :as cfg} request info profile]
(if profile (if profile
(let [sxf ((:create session) (:id profile)) (let [sxf ((:create session) (:id profile))
token (or (:invitation-token info) token (or (:invitation-token info)
(tokens :generate {:iss :auth (tokens/generate sprops {:iss :auth
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id (:id profile)})) :profile-id (:id profile)}))
params {:token token} params {:token token}
uri (-> (u/uri (:public-uri cfg)) uri (-> (u/uri (:public-uri cfg))
@ -448,7 +449,7 @@
:iss :prepared-register :iss :prepared-register
:is-active true :is-active true
:exp (dt/in-future {:hours 48})) :exp (dt/in-future {:hours 48}))
token (tokens :generate info) token (tokens/generate sprops info)
params (d/without-nils params (d/without-nils
{:token token {:token token
:fullname (:fullname info)}) :fullname (:fullname info)})
@ -458,13 +459,13 @@
(redirect-response uri)))) (redirect-response uri))))
(defn- auth-handler (defn- auth-handler
[{:keys [tokens] :as cfg} {:keys [params] :as request}] [{:keys [sprops] :as cfg} {:keys [params] :as request}]
(let [props (audit/extract-utm-params params) (let [props (audit/extract-utm-params params)
state (tokens :generate state (tokens/generate sprops
{:iss :oauth {:iss :oauth
:invitation-token (:invitation-token params) :invitation-token (:invitation-token params)
:props props :props props
:exp (dt/in-future "15m")}) :exp (dt/in-future "15m")})
uri (build-auth-uri cfg state)] uri (build-auth-uri cfg state)]
(yrs/response 200 {:redirect-uri uri}))) (yrs/response 200 {:redirect-uri uri})))
@ -496,16 +497,16 @@
:hint "provider not configured"))))))}) :hint "provider not configured"))))))})
(s/def ::public-uri ::us/not-empty-string) (s/def ::public-uri ::us/not-empty-string)
(s/def ::http-client fn?) (s/def ::http-client ::http/client)
(s/def ::session map?) (s/def ::session map?)
(s/def ::tokens fn?) (s/def ::sprops map?)
(s/def ::providers map?) (s/def ::providers map?)
(defmethod ig/pre-init-spec ::routes (defmethod ig/pre-init-spec ::routes
[_] [_]
(s/keys :req-un [::public-uri (s/keys :req-un [::public-uri
::session ::session
::tokens ::sprops
::http-client ::http-client
::providers ::providers
::db/pool ::db/pool

View file

@ -114,18 +114,18 @@
;; HTTP ROUTER ;; HTTP ROUTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::oauth map?)
(s/def ::storage map?)
(s/def ::assets map?) (s/def ::assets map?)
(s/def ::feedback fn?)
(s/def ::ws fn?)
(s/def ::audit-handler fn?) (s/def ::audit-handler fn?)
(s/def ::awsns-handler fn?) (s/def ::awsns-handler fn?)
(s/def ::session map?)
(s/def ::rpc-routes (s/nilable vector?))
(s/def ::debug-routes (s/nilable vector?)) (s/def ::debug-routes (s/nilable vector?))
(s/def ::oidc-routes (s/nilable vector?))
(s/def ::doc-routes (s/nilable vector?)) (s/def ::doc-routes (s/nilable vector?))
(s/def ::feedback fn?)
(s/def ::oauth map?)
(s/def ::oidc-routes (s/nilable vector?))
(s/def ::rpc-routes (s/nilable vector?))
(s/def ::session map?)
(s/def ::storage map?)
(s/def ::ws fn?)
(defmethod ig/pre-init-spec ::router [_] (defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::mtx/metrics (s/keys :req-un [::mtx/metrics

View file

@ -11,6 +11,8 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.http.client :as http]
[app.tokens :as tokens]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
@ -24,10 +26,11 @@
(declare parse-notification) (declare parse-notification)
(declare process-report) (declare process-report)
(s/def ::http-client fn?) (s/def ::http-client ::http/client)
(s/def ::sprops map?)
(defmethod ig/pre-init-spec ::handler [_] (defmethod ig/pre-init-spec ::handler [_]
(s/keys :req-un [::db/pool ::http-client])) (s/keys :req-un [::db/pool ::http-client ::sprops]))
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ {:keys [executor] :as cfg}] [_ {:keys [executor] :as cfg}]
@ -46,7 +49,7 @@
(let [surl (get body "SubscribeURL") (let [surl (get body "SubscribeURL")
stopic (get body "TopicArn")] stopic (get body "TopicArn")]
(l/info :action "subscription received" :topic stopic :url surl) (l/info :action "subscription received" :topic stopic :url surl)
(http-client {:uri surl :method :post :timeout 10000} {:sync? true})) (http/req! http-client {:uri surl :method :post :timeout 10000} {:sync? true}))
(= mtype "Notification") (= mtype "Notification")
(when-let [message (parse-json (get body "Message"))] (when-let [message (parse-json (get body "Message"))]
@ -97,10 +100,10 @@
(get mail "headers"))) (get mail "headers")))
(defn- extract-identity (defn- extract-identity
[{:keys [tokens] :as cfg} headers] [{:keys [sprops]} headers]
(let [tdata (get headers "x-penpot-data")] (let [tdata (get headers "x-penpot-data")]
(when-not (str/empty? tdata) (when-not (str/empty? tdata)
(let [result (tokens :verify {:token tdata :iss :profile-identity})] (let [result (tokens/verify sprops {:token tdata :iss :profile-identity})]
(:profile-id result))))) (:profile-id result)))))
(defn- parse-notification (defn- parse-notification

View file

@ -31,7 +31,6 @@
(http/send-async req {:client client :as response-type})))) (http/send-async req {:client client :as response-type}))))
{::client client}))) {::client client})))
(defn req! (defn req!
"A convencience toplevel function for gradual migration to a new API "A convencience toplevel function for gradual migration to a new API
convention." convention."

View file

@ -11,6 +11,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.tokens :as tokens]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -39,7 +40,7 @@
(delete-session [store key])) (delete-session [store key]))
(defn- make-database-store (defn- make-database-store
[{:keys [pool tokens executor]}] [{:keys [pool sprops executor]}]
(reify ISessionStore (reify ISessionStore
(read-session [_ token] (read-session [_ token]
(px/with-dispatch executor (px/with-dispatch executor
@ -50,9 +51,9 @@
(let [profile-id (:profile-id data) (let [profile-id (:profile-id data)
user-agent (:user-agent data) user-agent (:user-agent data)
created-at (or (:created-at data) (dt/now)) created-at (or (:created-at data) (dt/now))
token (tokens :generate {:iss "authentication" token (tokens/generate sprops {:iss "authentication"
:iat created-at :iat created-at
:uid profile-id}) :uid profile-id})
params {:user-agent user-agent params {:user-agent user-agent
:profile-id profile-id :profile-id profile-id
:created-at created-at :created-at created-at
@ -75,7 +76,7 @@
nil)))) nil))))
(defn make-inmemory-store (defn make-inmemory-store
[{:keys [tokens]}] [{:keys [sprops]}]
(let [cache (atom {})] (let [cache (atom {})]
(reify ISessionStore (reify ISessionStore
(read-session [_ token] (read-session [_ token]
@ -86,9 +87,9 @@
(let [profile-id (:profile-id data) (let [profile-id (:profile-id data)
user-agent (:user-agent data) user-agent (:user-agent data)
created-at (or (:created-at data) (dt/now)) created-at (or (:created-at data) (dt/now))
token (tokens :generate {:iss "authentication" token (tokens/generate sprops {:iss "authentication"
:iat created-at :iat created-at
:uid profile-id}) :uid profile-id})
params {:user-agent user-agent params {:user-agent user-agent
:created-at created-at :created-at created-at
:updated-at created-at :updated-at created-at
@ -108,9 +109,9 @@
(swap! cache dissoc token) (swap! cache dissoc token)
nil))))) nil)))))
(s/def ::tokens fn?) (s/def ::sprops map?)
(defmethod ig/pre-init-spec ::store [_] (defmethod ig/pre-init-spec ::store [_]
(s/keys :req-un [::db/pool ::wrk/executor ::tokens])) (s/keys :req-un [::db/pool ::wrk/executor ::sprops]))
(defmethod ig/init-key ::store (defmethod ig/init-key ::store
[_ {:keys [pool] :as cfg}] [_ {:keys [pool] :as cfg}]

View file

@ -15,6 +15,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.tokens :as tokens]
[app.util.async :as aa] [app.util.async :as aa]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk] [app.worker :as wrk]
@ -237,10 +238,10 @@
(s/def ::http-client fn?) (s/def ::http-client fn?)
(s/def ::uri ::us/string) (s/def ::uri ::us/string)
(s/def ::tokens fn?) (s/def ::sprops map?)
(defmethod ig/pre-init-spec ::archive-task [_] (defmethod ig/pre-init-spec ::archive-task [_]
(s/keys :req-un [::db/pool ::tokens ::http-client] (s/keys :req-un [::db/pool ::sprops ::http-client]
:opt-un [::uri])) :opt-un [::uri]))
(defmethod ig/init-key ::archive-task (defmethod ig/init-key ::archive-task
@ -276,7 +277,7 @@
for update skip locked;") for update skip locked;")
(defn archive-events (defn archive-events
[{:keys [pool uri tokens http-client] :as cfg}] [{:keys [pool uri sprops http-client] :as cfg}]
(letfn [(decode-row [{:keys [props ip-addr context] :as row}] (letfn [(decode-row [{:keys [props ip-addr context] :as row}]
(cond-> row (cond-> row
(db/pgobject? props) (db/pgobject? props)
@ -300,9 +301,9 @@
:context])) :context]))
(send [events] (send [events]
(let [token (tokens :generate {:iss "authentication" (let [token (tokens/generate sprops {:iss "authentication"
:iat (dt/now) :iat (dt/now)
:uid uuid/zero}) :uid uuid/zero})
body (t/encode {:events events}) body (t/encode {:events events})
headers {"content-type" "application/transit+json" headers {"content-type" "application/transit+json"
"origin" (cf/get :public-uri) "origin" (cf/get :public-uri)

View file

@ -69,9 +69,6 @@
:executor (ig/ref [::default :app.worker/executor]) :executor (ig/ref [::default :app.worker/executor])
:redis-uri (cf/get :redis-uri)} :redis-uri (cf/get :redis-uri)}
:app.tokens/tokens
{:keys (ig/ref :app.setup/keys)}
:app.storage.tmp/cleaner :app.storage.tmp/cleaner
{:executor (ig/ref [::worker :app.worker/executor]) {:executor (ig/ref [::worker :app.worker/executor])
:scheduler (ig/ref :app.worker/scheduler)} :scheduler (ig/ref :app.worker/scheduler)}
@ -92,7 +89,7 @@
:app.http.session/store :app.http.session/store
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens) :sprops (ig/ref :app.setup/props)
:executor (ig/ref [::default :app.worker/executor])} :executor (ig/ref [::default :app.worker/executor])}
:app.http.session/gc-task :app.http.session/gc-task
@ -100,7 +97,7 @@
:max-age (cf/get :auth-token-cookie-max-age)} :max-age (cf/get :auth-token-cookie-max-age)}
:app.http.awsns/handler :app.http.awsns/handler
{:tokens (ig/ref :app.tokens/tokens) {:sprops (ig/ref :app.setup/props)
:pool (ig/ref :app.db/pool) :pool (ig/ref :app.db/pool)
:http-client (ig/ref :app.http/client) :http-client (ig/ref :app.http/client)
:executor (ig/ref [::worker :app.worker/executor])} :executor (ig/ref [::worker :app.worker/executor])}
@ -168,13 +165,14 @@
:github (ig/ref :app.auth.oidc/github-provider) :github (ig/ref :app.auth.oidc/github-provider)
:gitlab (ig/ref :app.auth.oidc/gitlab-provider) :gitlab (ig/ref :app.auth.oidc/gitlab-provider)
:oidc (ig/ref :app.auth.oidc/generic-provider)} :oidc (ig/ref :app.auth.oidc/generic-provider)}
:tokens (ig/ref :app.tokens/tokens) :sprops (ig/ref :app.setup/props)
:http-client (ig/ref :app.http/client) :http-client (ig/ref :app.http/client)
:pool (ig/ref :app.db/pool) :pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session) :session (ig/ref :app.http/session)
:public-uri (cf/get :public-uri) :public-uri (cf/get :public-uri)
:executor (ig/ref [::default :app.worker/executor])} :executor (ig/ref [::default :app.worker/executor])}
;; TODO: revisit the dependencies of this service, looks they are too much unused of them
:app.http/router :app.http/router
{:assets (ig/ref :app.http.assets/handlers) {:assets (ig/ref :app.http.assets/handlers)
:feedback (ig/ref :app.http.feedback/handler) :feedback (ig/ref :app.http.feedback/handler)
@ -186,7 +184,6 @@
:metrics (ig/ref :app.metrics/metrics) :metrics (ig/ref :app.metrics/metrics)
:public-uri (cf/get :public-uri) :public-uri (cf/get :public-uri)
:storage (ig/ref :app.storage/storage) :storage (ig/ref :app.storage/storage)
:tokens (ig/ref :app.tokens/tokens)
:audit-handler (ig/ref :app.loggers.audit/http-handler) :audit-handler (ig/ref :app.loggers.audit/http-handler)
:rpc-routes (ig/ref :app.rpc/routes) :rpc-routes (ig/ref :app.rpc/routes)
:doc-routes (ig/ref :app.rpc.doc/routes) :doc-routes (ig/ref :app.rpc.doc/routes)
@ -218,7 +215,7 @@
:app.rpc/methods :app.rpc/methods
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session) :session (ig/ref :app.http/session)
:tokens (ig/ref :app.tokens/tokens) :sprops (ig/ref :app.setup/props)
:metrics (ig/ref :app.metrics/metrics) :metrics (ig/ref :app.metrics/metrics)
:storage (ig/ref :app.storage/storage) :storage (ig/ref :app.storage/storage)
:msgbus (ig/ref :app.msgbus/msgbus) :msgbus (ig/ref :app.msgbus/msgbus)
@ -293,8 +290,8 @@
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:key (cf/get :secret-key)} :key (cf/get :secret-key)}
:app.setup/keys ;; :app.setup/keys
{:props (ig/ref :app.setup/props)} ;; {:props (ig/ref :app.setup/props)}
:app.loggers.zmq/receiver :app.loggers.zmq/receiver
{:endpoint (cf/get :loggers-zmq-uri)} {:endpoint (cf/get :loggers-zmq-uri)}
@ -309,7 +306,7 @@
:app.loggers.audit/archive-task :app.loggers.audit/archive-task
{:uri (cf/get :audit-log-archive-uri) {:uri (cf/get :audit-log-archive-uri)
:tokens (ig/ref :app.tokens/tokens) :sprops (ig/ref :app.setup/props)
:pool (ig/ref :app.db/pool) :pool (ig/ref :app.db/pool)
:http-client (ig/ref :app.http/client)} :http-client (ig/ref :app.http/client)}

View file

@ -258,12 +258,12 @@
(s/def ::public-uri ::us/not-empty-string) (s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?) (s/def ::session map?)
(s/def ::storage some?) (s/def ::storage some?)
(s/def ::tokens fn?) (s/def ::sprops map?)
(defmethod ig/pre-init-spec ::methods [_] (defmethod ig/pre-init-spec ::methods [_]
(s/keys :req-un [::storage (s/keys :req-un [::storage
::session ::session
::tokens ::sprops
::audit ::audit
::executors ::executors
::public-uri ::public-uri

View file

@ -17,6 +17,7 @@
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.rpc.rlimit :as rlimit] [app.rpc.rlimit :as rlimit]
[app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.hashers :as hashers] [buddy.hashers :as hashers]
@ -80,7 +81,7 @@
;; ---- COMMAND: login with password ;; ---- COMMAND: login with password
(defn login-with-password (defn login-with-password
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}] [{:keys [pool session sprops] :as cfg} {:keys [email password] :as params}]
(when-not (contains? cf/flags :login) (when-not (contains? cf/flags :login)
(ex/raise :type :restriction (ex/raise :type :restriction
@ -114,7 +115,7 @@
(profile/decode-profile-row)) (profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation})) (tokens/verify sprops {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't loging with other email and ;; invitation because invitations matches exactly; and user can't loging with other email and
@ -156,9 +157,9 @@
;; ---- COMMAND: Recover Profile ;; ---- COMMAND: Recover Profile
(defn recover-profile (defn recover-profile
[{:keys [pool tokens] :as cfg} {:keys [token password]}] [{:keys [pool sprops] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token] (letfn [(validate-token [token]
(let [tdata (tokens :verify {:token token :iss :password-recovery})] (let [tdata (tokens/verify sprops {:token token :iss :password-recovery})]
(:profile-id tdata))) (:profile-id tdata)))
(update-password [conn profile-id] (update-password [conn profile-id]
@ -184,12 +185,12 @@
;; ---- COMMAND: Prepare Register ;; ---- COMMAND: Prepare Register
(defn prepare-register (defn prepare-register
[{:keys [pool tokens] :as cfg} params] [{:keys [pool sprops] :as cfg} params]
(when-not (contains? cf/flags :registration) (when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token) (if-not (contains? params :invitation-token)
(ex/raise :type :restriction (ex/raise :type :restriction
:code :registration-disabled) :code :registration-disabled)
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})] (let [invitation (tokens/verify sprops {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation)) (when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction (ex/raise :type :restriction
:code :email-does-not-match-invitation :code :email-does-not-match-invitation
@ -222,7 +223,7 @@
:iss :prepared-register :iss :prepared-register
:exp (dt/in-future "48h")} :exp (dt/in-future "48h")}
token (tokens :generate params)] token (tokens/generate sprops params)]
(with-meta {:token token} (with-meta {:token token}
{::audit/profile-id uuid/zero}))) {::audit/profile-id uuid/zero})))
@ -297,8 +298,8 @@
(assoc :default-project-id (:default-project-id team))))) (assoc :default-project-id (:default-project-id team)))))
(defn register-profile (defn register-profile
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}] [{:keys [conn sprops session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register}) (let [claims (tokens/verify sprops {:token token :iss :prepared-register})
params (merge params claims)] params (merge params claims)]
(check-profile-existence! conn params) (check-profile-existence! conn params)
(let [is-active (or (:is-active params) (let [is-active (or (:is-active params)
@ -308,14 +309,14 @@
(create-profile-relations conn) (create-profile-relations conn)
(profile/decode-profile-row)) (profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))] (tokens/verify sprops {:token token :iss :team-invitation}))]
(cond (cond
;; If invitation token comes in params, this is because the user comes from team-invitation process; ;; If invitation token comes in params, this is because the user comes from team-invitation process;
;; in this case, regenerate token and send back to the user a new invitation token (and mark current ;; in this case, regenerate token and send back to the user a new invitation token (and mark current
;; session as logged). This happens only if the invitation email matches with the register email. ;; session as logged). This happens only if the invitation email matches with the register email.
(and (some? invitation) (= (:email profile) (:member-email invitation))) (and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile)) (let [claims (assoc invitation :member-id (:id profile))
token (tokens :generate claims) token (tokens/generate sprops claims)
resp {:invitation-token token}] resp {:invitation-token token}]
(with-meta resp (with-meta resp
{:transform-response ((:create session) (:id profile)) {:transform-response ((:create session) (:id profile))
@ -341,14 +342,15 @@
;; In all other cases, send a verification email. ;; In all other cases, send a verification email.
:else :else
(let [vtoken (tokens :generate (let [vtoken (tokens/generate sprops
{:iss :verify-email {:iss :verify-email
:exp (dt/in-future "48h") :exp (dt/in-future "48h")
:profile-id (:id profile) :profile-id (:id profile)
:email (:email profile)}) :email (:email profile)})
ptoken (tokens :generate-predefined ptoken (tokens/generate sprops
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})] :profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(eml/send! {::eml/conn conn (eml/send! {::eml/conn conn
::eml/factory eml/register ::eml/factory eml/register
:public-uri (:public-uri cfg) :public-uri (:public-uri cfg)
@ -376,18 +378,19 @@
;; ---- COMMAND: Request Profile Recovery ;; ---- COMMAND: Request Profile Recovery
(defn request-profile-recovery (defn request-profile-recovery
[{:keys [pool tokens] :as cfg} {:keys [email] :as params}] [{:keys [pool sprops] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}] (letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens :generate (let [token (tokens/generate sprops
{:iss :password-recovery {:iss :password-recovery
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id id})] :profile-id id})]
(assoc profile :token token))) (assoc profile :token token)))
(send-email-notification [conn profile] (send-email-notification [conn profile]
(let [ptoken (tokens :generate-predefined (let [ptoken (tokens/generate sprops
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})] :profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(eml/send! {::eml/conn conn (eml/send! {::eml/conn conn
::eml/factory eml/password-recovery ::eml/factory eml/password-recovery
:public-uri (:public-uri cfg) :public-uri (:public-uri cfg)

View file

@ -19,6 +19,7 @@
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.rpc.rlimit :as rlimit] [app.rpc.rlimit :as rlimit]
[app.storage :as sto] [app.storage :as sto]
[app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -183,15 +184,16 @@
{:changed true}) {:changed true})
(defn- request-email-change (defn- request-email-change
[{:keys [conn tokens] :as cfg} {:keys [profile email] :as params}] [{:keys [conn sprops] :as cfg} {:keys [profile email] :as params}]
(let [token (tokens :generate (let [token (tokens/generate sprops
{:iss :change-email {:iss :change-email
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id (:id profile) :profile-id (:id profile)
:email email}) :email email})
ptoken (tokens :generate-predefined ptoken (tokens/generate sprops
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})] :profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(when (not= email (:email profile)) (when (not= email (:email profile))
(cmd.auth/check-profile-existence! conn params)) (cmd.auth/check-profile-existence! conn params))

View file

@ -22,6 +22,7 @@
[app.rpc.queries.teams :as teams] [app.rpc.queries.teams :as teams]
[app.rpc.rlimit :as rlimit] [app.rpc.rlimit :as rlimit]
[app.storage :as sto] [app.storage :as sto]
[app.tokens :as tokens]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -398,20 +399,21 @@
update set role = ?, valid_until = ?, updated_at = now();") update set role = ?, valid_until = ?, updated_at = now();")
(defn- create-team-invitation (defn- create-team-invitation
[{:keys [conn tokens team profile role email] :as cfg}] [{:keys [conn sprops team profile role email] :as cfg}]
(let [member (profile/retrieve-profile-data-by-email conn email) (let [member (profile/retrieve-profile-data-by-email conn email)
token-exp (dt/in-future "168h") ;; 7 days token-exp (dt/in-future "168h") ;; 7 days
itoken (tokens :generate itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp token-exp :exp token-exp
:profile-id (:id profile) :profile-id (:id profile)
:role role :role role
:team-id (:id team) :team-id (:id team)
:member-email (:email member email) :member-email (:email member email)
:member-id (:id member)}) :member-id (:id member)})
ptoken (tokens :generate-predefined ptoken (tokens/generate sprops
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})] :profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
(when (contains? cf/flags :log-invitation-tokens) (when (contains? cf/flags :log-invitation-tokens)
(l/trace :hint "invitation token" :token itoken)) (l/trace :hint "invitation token" :token itoken))

View file

@ -12,6 +12,8 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv] [app.util.services :as sv]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -23,9 +25,9 @@
:opt-un [::profile-id])) :opt-un [::profile-id]))
(sv/defmethod ::verify-token {:auth false} (sv/defmethod ::verify-token {:auth false}
[{:keys [pool tokens] :as cfg} {:keys [token] :as params}] [{:keys [pool sprops] :as cfg} {:keys [token] :as params}]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [claims (tokens :verify {:token token}) (let [claims (tokens/verify sprops {:token token})
cfg (assoc cfg :conn conn)] cfg (assoc cfg :conn conn)]
(process-token cfg params claims)))) (process-token cfg params claims))))
@ -76,19 +78,19 @@
(s/def ::iss keyword?) (s/def ::iss keyword?)
(s/def ::exp ::us/inst) (s/def ::exp ::us/inst)
(s/def :internal.tokens.team-invitation/profile-id ::us/uuid) (s/def ::spec.team-invitation/profile-id ::us/uuid)
(s/def :internal.tokens.team-invitation/role ::us/keyword) (s/def ::spec.team-invitation/role ::us/keyword)
(s/def :internal.tokens.team-invitation/team-id ::us/uuid) (s/def ::spec.team-invitation/team-id ::us/uuid)
(s/def :internal.tokens.team-invitation/member-email ::us/email) (s/def ::spec.team-invitation/member-email ::us/email)
(s/def :internal.tokens.team-invitation/member-id (s/nilable ::us/uuid)) (s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid))
(s/def ::team-invitation-claims (s/def ::team-invitation-claims
(s/keys :req-un [::iss ::exp (s/keys :req-un [::iss ::exp
:internal.tokens.team-invitation/profile-id ::spec.team-invitation/profile-id
:internal.tokens.team-invitation/role ::spec.team-invitation/role
:internal.tokens.team-invitation/team-id ::spec.team-invitation/team-id
:internal.tokens.team-invitation/member-email] ::spec.team-invitation/member-email]
:opt-un [:internal.tokens.team-invitation/member-id])) :opt-un [::spec.team-invitation/member-id]))
(defn- accept-invitation (defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims}] [{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims}]

View file

@ -11,6 +11,7 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.setup.builtin-templates] [app.setup.builtin-templates]
[app.setup.keys :as keys]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn] [buddy.core.nonce :as bn]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -59,6 +60,8 @@
"all sessions on each restart, it is hightly recommeded setting up the " "all sessions on each restart, it is hightly recommeded setting up the "
"PENPOT_SECRET_KEY environment variable"))) "PENPOT_SECRET_KEY environment variable")))
(let [stored (-> (retrieve-all conn) (let [secret (or key (generate-random-key))]
(assoc :secret-key (or key (generate-random-key))))] (-> (retrieve-all conn)
(update stored :instance-id handle-instance-id conn (db/read-only? pool))))) (assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens" :size 32))
(update :instance-id handle-instance-id conn (db/read-only? pool))))))

View file

@ -6,24 +6,17 @@
(ns app.setup.keys (ns app.setup.keys
"Keys derivation service." "Keys derivation service."
(:refer-clojure :exclude [derive])
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[buddy.core.kdf :as bk] [buddy.core.kdf :as bk]))
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(s/def ::secret-key ::us/string)
(s/def ::props (s/keys :req-un [::secret-key]))
(defmethod ig/pre-init-spec :app.setup/keys [_]
(s/keys :req-un [::props]))
(defmethod ig/init-key :app.setup/keys
[_ {:keys [props] :as cfg}]
(fn [& {:keys [salt _]}]
(let [engine (bk/engine {:key (:secret-key props)
:salt salt
:alg :hkdf
:digest :blake2b-512})]
(bk/get-bytes engine 32))))
(defn derive
"Derive a key from secret-key"
[secret-key & {:keys [salt size]}]
(us/assert! ::us/not-empty-string secret-key)
(let [engine (bk/engine {:key secret-key
:salt salt
:alg :hkdf
:digest :blake2b-512})]
(bk/get-bytes engine size)))

View file

@ -5,28 +5,27 @@
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.tokens (ns app.tokens
"Tokens generation service." "Tokens generation API."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.transit :as t] [app.common.transit :as t]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.sign.jwe :as jwe] [buddy.sign.jwe :as jwe]))
[clojure.spec.alpha :as s]
[integrant.core :as ig]))
(defn- generate (defn generate
[cfg claims] [{:keys [tokens-key]} claims]
(us/assert! ::us/not-empty-string tokens-key)
(let [payload (-> claims (let [payload (-> claims
(assoc :iat (dt/now)) (assoc :iat (dt/now))
(d/without-nils) (d/without-nils)
(t/encode))] (t/encode))]
(jwe/encrypt payload (::secret cfg) {:alg :a256kw :enc :a256gcm}))) (jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm})))
(defn- verify (defn verify
[cfg {:keys [token] :as params}] [{:keys [tokens-key]} {:keys [token] :as params}]
(let [payload (jwe/decrypt token (::secret cfg) {:alg :a256kw :enc :a256gcm}) (let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})
claims (t/decode payload)] claims (t/decode payload)]
(when (and (dt/instant? (:exp claims)) (when (and (dt/instant? (:exp claims))
(dt/is-before? (:exp claims) (dt/now))) (dt/is-before? (:exp claims) (dt/now)))
@ -45,30 +44,7 @@
:params params)) :params params))
claims)) claims))
(defn- generate-predefined
[cfg {:keys [iss profile-id] :as params}]
(case iss
:profile-identity
(do
(us/verify uuid? profile-id)
(generate cfg (assoc params
:exp (dt/in-future {:days 30}))))
(ex/raise :type :internal
:code :not-implemented
:hint "no predefined token")))
(s/def ::keys fn?)
(defmethod ig/pre-init-spec ::tokens [_]
(s/keys :req-un [::keys]))
(defmethod ig/init-key ::tokens
[_ {:keys [keys] :as cfg}]
(let [secret (keys :salt "tokens" :size 32)
cfg (assoc cfg ::secret secret)]
(fn [action params]
(case action
:generate-predefined (generate-predefined cfg params)
:verify (verify cfg params)
:generate (generate cfg params)))))

View file

@ -116,6 +116,7 @@
(some? position-modifier) (some? position-modifier)
(gpt/transform position-modifier)) (gpt/transform position-modifier))
content (:content draft) content (:content draft)
pos-x (* (:x position) zoom) pos-x (* (:x position) zoom)
pos-y (* (:y position) zoom) pos-y (* (:y position) zoom)