Improve invitation token validation

This commit is contained in:
Pablo Alba 2022-09-26 23:56:58 +02:00 committed by Andrey Antukh
parent b74631bf4a
commit 47363d96f1
6 changed files with 325 additions and 155 deletions

View file

@ -5,6 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.db
(:refer-clojure :exclude [get])
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
@ -270,28 +271,55 @@
(sql/delete table params opts)
(assoc opts :return-keys true))))
(defn- is-deleted?
(defn is-row-deleted?
[{:keys [deleted-at]}]
(and (dt/instant? deleted-at)
(< (inst-ms deleted-at)
(inst-ms (dt/now)))))
(defn get-by-params
(defn get*
"Internal function for retrieve a single row from database that
matches a simple filters."
([ds table params]
(get-by-params ds table params nil))
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
(let [res (exec-one! ds (sql/select table params opts))]
(when (and check-not-found (or (not res) (is-deleted? res)))
(get* ds table params nil))
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
(let [rows (exec! ds (sql/select table params opts))
rows (cond->> rows
check-deleted?
(remove is-row-deleted?))]
(first rows))))
(defn get
([ds table params]
(get ds table params nil))
([ds table params {:keys [check-deleted?] :or {check-deleted? true} :as opts}]
(let [row (get* ds table params opts)]
(when (and (not row) check-deleted?)
(ex/raise :type :not-found
:table table
:hint "database object not found"))
res)))
row)))
(defn get-by-params
"DEPRECATED"
([ds table params]
(get-by-params ds table params nil))
([ds table params {:keys [check-not-found] :or {check-not-found true} :as opts}]
(let [row (get* ds table params (assoc opts :check-deleted? check-not-found))]
(when (and (not row) check-not-found)
(ex/raise :type :not-found
:table table
:hint "database object not found"))
row)))
(defn get-by-id
([ds table id]
(get-by-params ds table {:id id} nil))
(get ds table {:id id} nil))
([ds table id opts]
(get-by-params ds table {:id id} opts)))
(let [opts (cond-> opts
(contains? opts :check-not-found)
(assoc :check-deleted? (:check-not-found opts)))]
(get ds table {:id id} opts))))
(defn query
([ds table params]

View file

@ -80,16 +80,19 @@
;; --- Team Invitation
(defn- accept-invitation
[{:keys [conn] :as cfg} {:keys [member-id team-id role member-email] :as claims} invitation]
(let [member (profile/retrieve-profile conn member-id)
;; Update the role if there is an invitation
[{:keys [conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge
{:team-id team-id
:profile-id member-id}
:profile-id (:id member)}
(teams/role->params role))]
;; Do not allow blocked users accept invitations.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {:on-conflict-do-nothing true})
@ -98,7 +101,7 @@
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id member-id}))
{:id (:id member)}))
;; Delete the invitation
(db/delete! conn :team-invitation
@ -106,7 +109,6 @@
(assoc member :is-active true)))
(s/def ::spec.team-invitation/profile-id ::us/uuid)
(s/def ::spec.team-invitation/role ::us/keyword)
(s/def ::spec.team-invitation/team-id ::us/uuid)
@ -122,23 +124,28 @@
:opt-un [::spec.team-invitation/member-id]))
(defmethod process-token :team-invitation
[{:keys [conn session] :as cfg} {:keys [profile-id token]} {:keys [member-id team-id member-email] :as claims}]
[{:keys [conn session] :as cfg} {:keys [profile-id token]}
{:keys [member-id team-id member-email] :as claims}]
(us/assert ::team-invitation-claims claims)
(let [invitation (db/get-by-params conn :team-invitation
{:team-id team-id :email-to member-email}
{:check-not-found false})]
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})
profile (db/get* conn :profile
{:id profile-id}
{:columns [:id :email]})]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token
:hint "no invitation associated with the token"))
(cond
;; This happens when token is filled with member-id and current
;; user is already logged in with exactly invited account.
(and (uuid? profile-id) (uuid? member-id))
(if (= member-id profile-id)
(let [profile (accept-invitation cfg claims invitation)]
(if (some? profile)
(if (or (= member-id profile-id)
(= member-email (:email profile)))
;; if we have logged-in user and it matches the invitation we
;; proceed with accepting the invitation and joining the
;; current profile to the invited team.
(let [profile (accept-invitation cfg claims invitation profile)]
(with-meta
(assoc claims :state :created)
{::audit/name "accept-team-invitation"
@ -146,40 +153,36 @@
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
::audit/profile-id profile-id}))
(ex/raise :type :validation
:code :invalid-token
:hint "logged-in user does not matches the invitation"))
;; This happens when an unlogged user, uses an invitation link.
(and (not profile-id) (uuid? member-id))
(let [profile (accept-invitation cfg claims invitation)]
(with-meta
(assoc claims :state :created)
{:transform-response ((:create session) (:id profile))
::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
;; If we have not logged-in user, we try find the invited
;; profile by member-id or member-email props of the invitation
;; token; If profile is found, we accept the invitation and
;; leave the user logged-in.
(if-let [member (db/get* conn :profile
(if member-id
{:id member-id}
{:email member-email})
{:columns [:id :email]})]
(let [profile (accept-invitation cfg claims invitation member)]
(with-meta
(assoc claims :state :created)
{:transform-response ((:create session) (:id profile))
::audit/name "accept-team-invitation"
::audit/props (merge
(audit/profile->props profile)
{:team-id (:team-id claims)
:role (:role claims)})
::audit/profile-id member-id}))
;; This case means that invitation token does not match with
;; registred user, so we need to indicate to frontend to redirect
;; it to register page.
(and (not profile-id) (nil? member-id))
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}
;; In all other cases, just tell to fontend to redirect the user
;; to the login page.
:else
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-login
:state :pending})))
{:invitation-token token
:iss :team-invitation
:redirect-to :auth-register
:state :pending}))))
;; --- Default

View file

@ -376,18 +376,17 @@
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
(doseq [email emails]
(create-team-invitation
(assoc cfg
:email email
:conn conn
:team team
:profile profile
:role role))
)
(with-meta {}
{::audit/props {:invitations (count emails)}}))))
(let [invitations (->> emails
(map (fn [email]
(assoc cfg
:email email
:conn conn
:team team
:profile profile
:role role)))
(map create-team-invitation))]
(with-meta (vec invitations)
{::audit/props {:invitations (count invitations)}})))))
(def sql:upsert-team-invitation
"insert into team_invitation(team_id, email_to, role, valid_until)
@ -449,10 +448,7 @@
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)}))
(assoc member :is-active true))
{:id (:id member)})))
(do
(db/exec-one! conn [sql:upsert-team-invitation
(:id team) (str/lower email) (name role)
@ -464,7 +460,9 @@
:invited-by (:fullname profile)
:team (:name team)
:token itoken
:extra-data ptoken})))))
:extra-data ptoken})))
itoken))
;; --- Mutation: Create Team & Invite Members

View file

@ -26,10 +26,14 @@
(t/encode))]
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm})))
(defn decode
[{:keys [tokens-key]} token]
(let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})]
(t/decode payload)))
(defn verify
[{:keys [tokens-key]} {:keys [token] :as params}]
(let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})
claims (t/decode payload)]
[sprops {:keys [token] :as params}]
(let [claims (decode sprops token)]
(when (and (dt/instant? (:exp claims))
(dt/is-before? (:exp claims) (dt/now)))
(ex/raise :type :validation