Improve auth module.

This commit is contained in:
Andrey Antukh 2021-02-11 13:36:46 +01:00
parent d5ff5ea91e
commit 5858f3f180
12 changed files with 269 additions and 254 deletions

View file

@ -12,7 +12,6 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cfg]
[app.http.auth :as auth]
[app.http.errors :as errors] [app.http.errors :as errors]
[app.http.middleware :as middleware] [app.http.middleware :as middleware]
[app.metrics :as mtx] [app.metrics :as mtx]
@ -147,9 +146,6 @@
["/github" {:post (:auth-handler github-auth)}] ["/github" {:post (:auth-handler github-auth)}]
["/github/callback" {:get (:callback-handler github-auth)}]] ["/github/callback" {:get (:callback-handler github-auth)}]]
["/login" {:post #(auth/login-handler cfg %)}]
["/logout" {:post #(auth/logout-handler cfg %)}]
["/login-ldap" {:post ldap-auth}] ["/login-ldap" {:post ldap-auth}]
["/rpc" {:middleware [(:middleware session)]} ["/rpc" {:middleware [(:middleware session)]}

View file

@ -1,31 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.http.auth
(:require
[app.http.session :as session]))
(defn login-handler
[{:keys [session rpc] :as cfg} request]
(let [data (:params request)
uagent (get-in request [:headers "user-agent"])
method (get-in rpc [:methods :mutation :login])
profile (method data)
id (session/create! session {:profile-id (:id profile)
:user-agent uagent})]
{:status 200
:cookies (session/cookies session {:value id})
:body profile}))
(defn logout-handler
[{:keys [session] :as cfg} request]
(session/delete! cfg request)
{:status 204
:cookies (session/cookies session {:value "" :max-age -1})
:body ""})

View file

@ -12,7 +12,6 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cfg]
[app.http.session :as session]
[app.util.http :as http] [app.util.http :as http]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.data.json :as json] [clojure.data.json :as json]
@ -38,7 +37,6 @@
(def scope "user:email") (def scope "user:email")
(defn- build-redirect-url (defn- build-redirect-url
[cfg] [cfg]
(let [public (u/uri (:public-uri cfg))] (let [public (u/uri (:public-uri cfg))]
@ -46,57 +44,47 @@
(defn- get-access-token (defn- get-access-token
[cfg state code] [cfg state code]
(let [params {:client_id (:client-id cfg) (try
:client_secret (:client-secret cfg) (let [params {:client_id (:client-id cfg)
:code code :client_secret (:client-secret cfg)
:state state :code code
:redirect_uri (build-redirect-url cfg)} :state state
req {:method :post :redirect_uri (build-redirect-url cfg)}
:headers {"content-type" "application/x-www-form-urlencoded" req {:method :post
"accept" "application/json"} :headers {"content-type" "application/x-www-form-urlencoded"
:uri (str token-url) "accept" "application/json"}
:body (u/map->query-string params)} :uri (str token-url)
res (http/send! req)] :timeout 6000
:body (u/map->query-string params)}
res (http/send! req)]
(when (not= 200 (:status res)) (when (= 200 (:status res))
(ex/raise :type :internal (-> (json/read-str (:body res))
:code :invalid-response-from-github (get "access_token"))))
:context {:status (:status res)
:body (:body res)})) (catch Exception e
(try (log/error e "unexpected error on get-access-token")
(let [data (json/read-str (:body res))] nil)))
(get data "access_token"))
(catch Throwable e
(log/error "unexpected error on parsing response body from github access token request" e)
nil))))
(defn- get-user-info (defn- get-user-info
[token] [token]
(let [req {:uri (str user-info-url) (try
:headers {"authorization" (str "token " token)} (let [req {:uri (str user-info-url)
:method :get} :headers {"authorization" (str "token " token)}
res (http/send! req)] :timeout 6000
:method :get}
(when (not= 200 (:status res)) res (http/send! req)]
(ex/raise :type :internal (when (= 200 (:status res))
:code :invalid-response-from-github (let [data (json/read-str (:body res))]
:context {:status (:status res) {:email (get data "email")
:body (:body res)})) :fullname (get data "name")})))
(catch Exception e
(try (log/error e "unexpected exception on get-user-info")
(let [data (json/read-str (:body res))] nil)))
{:email (get data "email")
:fullname (get data "name")})
(catch Throwable e
(log/error "unexpected error on parsing response body from github access token request" e)
nil))))
(defn auth (defn auth
[{:keys [tokens] :as cfg} _request] [{:keys [tokens] :as cfg} _request]
(let [state (tokens :generate (let [state (tokens :generate {:iss :github-oauth :exp (dt/in-future "15m")})
{:iss :github-oauth
:exp (dt/in-future "15m")})
params {:client_id (:client-id cfg/config) params {:client_id (:client-id cfg/config)
:redirect_uri (build-redirect-url cfg) :redirect_uri (build-redirect-url cfg)
:state state :state state
@ -109,37 +97,37 @@
(defn callback (defn callback
[{:keys [tokens rpc session] :as cfg} request] [{:keys [tokens rpc session] :as cfg} request]
(let [state (get-in request [:params :state]) (try
_ (tokens :verify {:token state :iss :github-oauth}) (let [state (get-in request [:params :state])
info (some->> (get-in request [:params :code]) _ (tokens :verify {:token state :iss :github-oauth})
(get-access-token cfg state) info (some->> (get-in request [:params :code])
(get-user-info))] (get-access-token cfg state)
(get-user-info))
(when-not info _ (when-not info
(ex/raise :type :authentication (ex/raise :type :internal
:code :unable-to-authenticate-with-github)) :code :unable-to-auth))
(let [method-fn (get-in rpc [:methods :mutation :login-or-register]) method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info) profile (method-fn {:email (:email info)
:fullname (:fullname info)}) :fullname (:fullname info)})
uagent (get-in request [:headers "user-agent"])
token (tokens :generate token (tokens :generate
{:iss :auth {:iss :auth
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id (:id profile)}) :profile-id (:id profile)})
uri (-> (u/uri (:public-uri cfg/config)) uri (-> (u/uri (:public-uri cfg/config))
(assoc :path "/#/auth/verify-token") (assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string {:token token}))) (assoc :query (u/map->query-string {:token token})))
sxf ((:create session) (:id profile))
sid (session/create! session {:profile-id (:id profile) rsp {:status 302 :headers {"location" (str uri)} :body ""}]
:user-agent uagent})] (sxf request rsp))
(catch Exception _e
{:status 302 (let [uri (-> (u/uri (:public-uri cfg))
:headers {"location" (str uri)} (assoc :path "/#/auth/login")
:cookies (session/cookies session/cookies {:value sid}) (assoc :query (u/map->query-string {:error "unable-to-auth"})))]
:body ""}))) {:status 302
:headers {"location" (str uri)}
:body ""}))))
;; --- ENTRY POINT ;; --- ENTRY POINT

View file

@ -12,88 +12,75 @@
[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.http.session :as session]
[app.util.http :as http] [app.util.http :as http]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.data.json :as json] [clojure.data.json :as json]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[integrant.core :as ig] [integrant.core :as ig]
[lambdaisland.uri :as uri])) [lambdaisland.uri :as u]))
(def scope "read_user") (def scope "read_user")
(defn- build-redirect-url (defn- build-redirect-url
[cfg] [cfg]
(let [public (uri/uri (:public-uri cfg))] (let [public (u/uri (:public-uri cfg))]
(str (assoc public :path "/api/oauth/gitlab/callback")))) (str (assoc public :path "/api/oauth/gitlab/callback"))))
(defn- build-oauth-uri (defn- build-oauth-uri
[cfg] [cfg]
(let [base-uri (uri/uri (:base-uri cfg))] (let [base-uri (u/uri (:base-uri cfg))]
(assoc base-uri :path "/oauth/authorize"))) (assoc base-uri :path "/oauth/authorize")))
(defn- build-token-url (defn- build-token-url
[cfg] [cfg]
(let [base-uri (uri/uri (:base-uri cfg))] (let [base-uri (u/uri (:base-uri cfg))]
(str (assoc base-uri :path "/oauth/token")))) (str (assoc base-uri :path "/oauth/token"))))
(defn- build-user-info-url (defn- build-user-info-url
[cfg] [cfg]
(let [base-uri (uri/uri (:base-uri cfg))] (let [base-uri (u/uri (:base-uri cfg))]
(str (assoc base-uri :path "/api/v4/user")))) (str (assoc base-uri :path "/api/v4/user"))))
(defn- get-access-token (defn- get-access-token
[cfg code] [cfg code]
(let [params {:client_id (:client-id cfg) (try
:client_secret (:client-secret cfg) (let [params {:client_id (:client-id cfg)
:code code :client_secret (:client-secret cfg)
:grant_type "authorization_code" :code code
:redirect_uri (build-redirect-url cfg)} :grant_type "authorization_code"
req {:method :post :redirect_uri (build-redirect-url cfg)}
:headers {"content-type" "application/x-www-form-urlencoded"} req {:method :post
:uri (build-token-url cfg) :headers {"content-type" "application/x-www-form-urlencoded"}
:body (uri/map->query-string params)} :uri (build-token-url cfg)
:body (u/map->query-string params)}
res (http/send! req)] res (http/send! req)]
(when (not= 200 (:status res)) (when (= 200 (:status res))
(ex/raise :type :internal (-> (json/read-str (:body res))
:code :invalid-response-from-gitlab (get "access_token"))))
:context {:status (:status res)
:body (:body res)}))
(try
(let [data (json/read-str (:body res))]
(get data "access_token"))
(catch Throwable e
(log/error "unexpected error on parsing response body from gitlab access token request" e)
nil))))
(catch Exception e
(log/error e "unexpected error on get-access-token")
nil)))
(defn- get-user-info (defn- get-user-info
[cfg token] [cfg token]
(let [req {:uri (build-user-info-url cfg) (try
:headers {"Authorization" (str "Bearer " token)} (let [req {:uri (build-user-info-url cfg)
:method :get} :headers {"Authorization" (str "Bearer " token)}
res (http/send! req)] :timeout 6000
:method :get}
res (http/send! req)]
(when (not= 200 (:status res)) (when (= 200 (:status res))
(ex/raise :type :internal (let [data (json/read-str (:body res))]
:code :invalid-response-from-gitlab {:email (get data "email")
:context {:status (:status res) :fullname (get data "name")})))
:body (:body res)}))
(try (catch Exception e
(let [data (json/read-str (:body res))] (log/error e "unexpected exception on get-user-info")
;; (clojure.pprint/pprint data) nil)))
{:email (get data "email")
:fullname (get data "name")})
(catch Throwable e
(log/error "unexpected error on parsing response body from gitlab access token request" e)
nil))))
(defn auth (defn auth
[{:keys [tokens] :as cfg} _request] [{:keys [tokens] :as cfg} _request]
@ -105,7 +92,7 @@
:response_type "code" :response_type "code"
:state token :state token
:scope scope} :scope scope}
query (uri/map->query-string params) query (u/map->query-string params)
uri (-> (build-oauth-uri cfg) uri (-> (build-oauth-uri cfg)
(assoc :query query))] (assoc :query query))]
{:status 200 {:status 200
@ -113,36 +100,37 @@
(defn callback (defn callback
[{:keys [tokens rpc session] :as cfg} request] [{:keys [tokens rpc session] :as cfg} request]
(let [token (get-in request [:params :state]) (try
_ (tokens :verify {:token token :iss :gitlab-oauth}) (let [token (get-in request [:params :state])
info (some->> (get-in request [:params :code]) _ (tokens :verify {:token token :iss :gitlab-oauth})
(get-access-token cfg) info (some->> (get-in request [:params :code])
(get-user-info cfg))] (get-access-token cfg)
(get-user-info cfg))
_ (when-not info
(ex/raise :type :internal
:code :unable-to-auth))
(when-not info method-fn (get-in rpc [:methods :mutation :login-or-register])
(ex/raise :type :authentication
:code :unable-to-authenticate-with-gitlab))
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info) profile (method-fn {:email (:email info)
:fullname (:fullname info)}) :fullname (:fullname info)})
uagent (get-in request [:headers "user-agent"])
token (tokens :generate {:iss :auth token (tokens :generate {:iss :auth
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id (:id profile)}) :profile-id (:id profile)})
uri (-> (uri/uri (:public-uri cfg)) uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token") (assoc :path "/#/auth/verify-token")
(assoc :query (uri/map->query-string {:token token}))) (assoc :query (u/map->query-string {:token token})))
sid (session/create! session {:profile-id (:id profile)
:user-agent uagent})]
{:status 302
:headers {"location" (str uri)}
:cookies (session/cookies session {:value sid})
:body ""})))
sxf ((:create session) (:id profile))
rsp {:status 302 :headers {"location" (str uri)} :body ""}]
(sxf request rsp))
(catch Exception _e
(let [uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth"})))]
{:status 302
:headers {"location" (str uri)}
:body ""}))))
(s/def ::client-id ::us/not-empty-string) (s/def ::client-id ::us/not-empty-string)
(s/def ::client-secret ::us/not-empty-string) (s/def ::client-secret ::us/not-empty-string)

View file

@ -11,14 +11,13 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.http.session :as session]
[app.util.http :as http] [app.util.http :as http]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.data.json :as json] [clojure.data.json :as json]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.tools.logging :as log] [clojure.tools.logging :as log]
[integrant.core :as ig] [integrant.core :as ig]
[lambdaisland.uri :as uri])) [lambdaisland.uri :as u]))
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth") (def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
@ -30,7 +29,7 @@
(defn- build-redirect-url (defn- build-redirect-url
[cfg] [cfg]
(let [public (uri/uri (:public-uri cfg))] (let [public (u/uri (:public-uri cfg))]
(str (assoc public :path "/api/oauth/google/callback")))) (str (assoc public :path "/api/oauth/google/callback"))))
(defn- get-access-token (defn- get-access-token
@ -44,7 +43,7 @@
req {:method :post req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"} :headers {"content-type" "application/x-www-form-urlencoded"}
:uri "https://oauth2.googleapis.com/token" :uri "https://oauth2.googleapis.com/token"
:body (uri/map->query-string params)} :body (u/map->query-string params)}
res (http/send! req)] res (http/send! req)]
(when (= 200 (:status res)) (when (= 200 (:status res))
@ -80,8 +79,8 @@
:response_type "code" :response_type "code"
:redirect_uri (build-redirect-url cfg) :redirect_uri (build-redirect-url cfg)
:client_id (:client-id cfg)} :client_id (:client-id cfg)}
query (uri/map->query-string params) query (u/map->query-string params)
uri (-> (uri/uri base-goauth-uri) uri (-> (u/uri base-goauth-uri)
(assoc :query query))] (assoc :query query))]
{:status 200 {:status 200
:body {:redirect-uri (str uri)}})) :body {:redirect-uri (str uri)}}))
@ -100,24 +99,20 @@
method-fn (get-in rpc [:methods :mutation :login-or-register]) method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info) profile (method-fn {:email (:email info)
:fullname (:fullname info)}) :fullname (:fullname info)})
uagent (get-in request [:headers "user-agent"])
token (tokens :generate {:iss :auth token (tokens :generate {:iss :auth
:exp (dt/in-future "15m") :exp (dt/in-future "15m")
:profile-id (:id profile)}) :profile-id (:id profile)})
uri (-> (uri/uri (:public-uri cfg)) uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token") (assoc :path "/#/auth/verify-token")
(assoc :query (uri/map->query-string {:token token}))) (assoc :query (u/map->query-string {:token token})))
sid (session/create! session {:profile-id (:id profile) sxf ((:create session) (:id profile))
:user-agent uagent})] rsp {:status 302 :headers {"location" (str uri)} :body ""}]
{:status 302 (sxf request rsp))
:headers {"location" (str uri)}
:cookies (session/cookies session {:value sid})
:body ""})
(catch Exception _e (catch Exception _e
(let [uri (-> (uri/uri (:public-uri cfg)) (let [uri (-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/login") (assoc :path "/#/auth/login")
(assoc :query (uri/map->query-string {:error "unable-to-auth"})))] (assoc :query (u/map->query-string {:error "unable-to-auth"})))]
{:status 302 {:status 302
:headers {"location" (str uri)} :headers {"location" (str uri)}
:body ""})))) :body ""}))))

View file

@ -11,7 +11,6 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.config :as cfg] [app.config :as cfg]
[app.http.session :as session]
[clj-ldap.client :as client] [clj-ldap.client :as client]
[clojure.set :as set] [clojure.set :as set]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
@ -66,12 +65,10 @@
(let [method-fn (get-in rpc [:methods :mutation :login-or-register]) (let [method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info) profile (method-fn {:email (:email info)
:fullname (:fullname info)}) :fullname (:fullname info)})
uagent (get-in request [:headers "user-agent"])
sid (session/create! session {:profile-id (:id profile) sxf ((:create session) (:id profile))
:user-agent uagent})] rsp {:status 200 :body profile}]
{:status 200 (sxf request rsp)))))
:cookies (session/cookies session {:value sid})
:body profile}))))
{::conn conn}))) {::conn conn})))
(defmethod ig/halt-key! ::client (defmethod ig/halt-key! ::client

View file

@ -16,14 +16,16 @@
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig])) [integrant.core :as ig]))
(defn next-session-id ;; --- IMPL
(defn- next-session-id
([] (next-session-id 96)) ([] (next-session-id 96))
([n] ([n]
(-> (bn/random-nonce n) (-> (bn/random-nonce n)
(bc/bytes->b64u) (bc/bytes->b64u)
(bc/bytes->str)))) (bc/bytes->str))))
(defn create! (defn- create
[{:keys [conn] :as cfg} {:keys [profile-id user-agent]}] [{:keys [conn] :as cfg} {:keys [profile-id user-agent]}]
(let [id (next-session-id)] (let [id (next-session-id)]
(db/insert! conn :http-session {:id id (db/insert! conn :http-session {:id id
@ -31,28 +33,28 @@
:user-agent user-agent}) :user-agent user-agent})
id)) id))
(defn delete! (defn- delete
[{:keys [conn cookie-name] :as cfg} request] [{:keys [conn cookie-name] :as cfg} {:keys [cookies] :as request}]
(when-let [token (get-in request [:cookies cookie-name :value])] (when-let [token (get-in cookies [cookie-name :value])]
(db/delete! conn :http-session {:id token})) (db/delete! conn :http-session {:id token}))
nil) nil)
(defn retrieve (defn- retrieve
[{:keys [conn] :as cfg} token] [{:keys [conn] :as cfg} token]
(when token (when token
(-> (db/exec-one! conn ["select profile_id from http_session where id = ?" token]) (-> (db/exec-one! conn ["select profile_id from http_session where id = ?" token])
(:profile-id)))) (:profile-id))))
(defn retrieve-from-request (defn- retrieve-from-request
[{:keys [cookie-name] :as cfg} request] [{:keys [cookie-name] :as cfg} {:keys [cookies] :as request}]
(->> (get-in request [:cookies cookie-name :value]) (->> (get-in cookies [cookie-name :value])
(retrieve cfg))) (retrieve cfg)))
(defn cookies (defn- cookies
[{:keys [cookie-name] :as cfg} vals] [{:keys [cookie-name] :as cfg} vals]
{cookie-name (merge vals {:path "/" :http-only true})}) {cookie-name (merge vals {:path "/" :http-only true})})
(defn middleware (defn- middleware
[cfg handler] [cfg handler]
(fn [request] (fn [request]
(if-let [profile-id (retrieve-from-request cfg request)] (if-let [profile-id (retrieve-from-request cfg request)]
@ -61,6 +63,8 @@
(handler (assoc request :profile-id profile-id))) (handler (assoc request :profile-id profile-id)))
(handler request)))) (handler request))))
;; --- STATE INIT
(defmethod ig/pre-init-spec ::session [_] (defmethod ig/pre-init-spec ::session [_]
(s/keys :req-un [::db/pool])) (s/keys :req-un [::db/pool]))
@ -71,4 +75,17 @@
(defmethod ig/init-key ::session (defmethod ig/init-key ::session
[_ {:keys [pool] :as cfg}] [_ {:keys [pool] :as cfg}]
(let [cfg (assoc cfg :conn pool)] (let [cfg (assoc cfg :conn pool)]
(merge cfg {:middleware #(middleware cfg %)}))) (-> cfg
(assoc :middleware #(middleware cfg %))
(assoc :create (fn [profile-id]
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
value (create cfg {:profile-id profile-id :user-agent uagent})]
(assoc response :cookies (cookies cfg {:value value}))))))
(assoc :delete (fn [request response]
(delete cfg request)
(assoc response
:status 204
:body ""
:cookies (cookies cfg {:value "" :max-age -1})))))))

View file

@ -16,7 +16,6 @@
[app.db :as db] [app.db :as db]
[app.db.profile-initial-data :refer [create-profile-initial-data]] [app.db.profile-initial-data :refer [create-profile-initial-data]]
[app.emails :as emails] [app.emails :as emails]
[app.http.session :as session]
[app.media :as media] [app.media :as media]
[app.rpc.mutations.projects :as projects] [app.rpc.mutations.projects :as projects]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
@ -95,13 +94,7 @@
(with-meta (assoc profile (with-meta (assoc profile
:is-active true :is-active true
:claims claims) :claims claims)
{:transform-response {:transform-response ((:create session) (:id profile))}))
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
id (session/create! session {:profile-id (:id profile)
:user-agent uagent})]
(assoc response
:cookies (session/cookies session {:value id}))))}))
;; If no token is provided, send a verification email ;; If no token is provided, send a verification email
(let [token (tokens :generate (let [token (tokens :generate
@ -217,7 +210,7 @@
:opt-un [::scope])) :opt-un [::scope]))
(sv/defmethod ::login {:auth false :rlimit :password} (sv/defmethod ::login {:auth false :rlimit :password}
[{:keys [pool] :as cfg} {:keys [email password scope] :as params}] [{:keys [pool session] :as cfg} {:keys [email password scope] :as params}]
(letfn [(check-password [profile password] (letfn [(check-password [profile password]
(when (= (:password profile) "!") (when (= (:password profile) "!")
(ex/raise :type :validation (ex/raise :type :validation
@ -240,8 +233,21 @@
(let [prof (-> (profile/retrieve-profile-data-by-email conn email) (let [prof (-> (profile/retrieve-profile-data-by-email conn email)
(validate-profile) (validate-profile)
(profile/strip-private-attrs)) (profile/strip-private-attrs))
addt (profile/retrieve-additional-data conn (:id prof))] addt (profile/retrieve-additional-data conn (:id prof))
(merge prof addt))))) prof (merge prof addt)]
(with-meta prof
{:transform-response ((:create session) (:id prof))})))))
;; --- Mutation: Logout
(s/def ::logout
(s/keys :req-un [::profile-id]))
(sv/defmethod ::logout
[{:keys [pool session] :as cfg} {:keys [profile-id] :as params}]
(with-meta {}
{:transform-response (:delete session)}))
;; --- Mutation: Register if not exists ;; --- Mutation: Register if not exists
@ -480,11 +486,7 @@
{:id profile-id}) {:id profile-id})
(with-meta {} (with-meta {}
{:transform-response {:transform-response (:delete session)})))
(fn [request response]
(session/delete! session request)
(assoc response
:cookies (session/cookies session {:value "" :max-age -1})))})))
(def sql:owned-teams (def sql:owned-teams
"with owner_teams as ( "with owner_teams as (

View file

@ -5,14 +5,13 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.rpc.mutations.verify-token (ns app.rpc.mutations.verify-token
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.http.session :as session]
[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.util.services :as sv] [app.util.services :as sv]
@ -57,14 +56,7 @@
{:id (:id profile)})) {:id (:id profile)}))
(with-meta claims (with-meta claims
{:transform-response {:transform-response ((:create session) profile-id)})))
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
id (session/create! session {:profile-id profile-id
:user-agent uagent})]
(assoc response
:cookies (session/cookies session {:value id}))))})))
(defmethod process-token :auth (defmethod process-token :auth
[{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] [{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}]
@ -116,13 +108,7 @@
;; the user clicking the link he already has access to the ;; the user clicking the link he already has access to the
;; email account. ;; email account.
(with-meta claims (with-meta claims
{:transform-response {:transform-response ((:create session) member-id)})))
(fn [request response]
(let [uagent (get-in request [:headers "user-agent"])
id (session/create! session {:profile-id member-id
:user-agent uagent})]
(assoc response
:cookies (session/cookies session {:value id}))))})))
;; In this case, we wait until frontend app redirect user to ;; In this case, we wait until frontend app redirect user to
;; registeration page, the user is correctly registered and the ;; registeration page, the user is correctly registered and the

View file

@ -191,3 +191,94 @@
;; TODO: profile deletion with owner teams ;; TODO: profile deletion with owner teams
;; TODO: profile registration ;; TODO: profile registration
;; TODO: profile password recovery ;; TODO: profile password recovery
(t/deftest test-register-when-registration-disabled
(with-mocks [mock {:target 'app.config/get
:return (th/mock-config-get-with
{:registration-enabled false})}]
(let [data {::th/type :register-profile
:email "user@example.com"
:password "foobar"
:fullname "foobar"}
out (th/mutation! data)
error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :restriction))
(t/is (= (:code edata) :registration-disabled)))))
(t/deftest test-register-existing-profile
(let [profile (th/create-profile* 1)
data {::th/type :register-profile
:email (:email profile)
:password "foobar"
:fullname "foobar"}
out (th/mutation! data)
error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :email-already-exists))))
(t/deftest test-register-profile
(with-mocks [mock {:target 'app.emails/send!
:return nil}]
(let [pool (:app.db/pool th/*system*)
data {::th/type :register-profile
:email "user@example.com"
:password "foobar"
:fullname "foobar"}
out (th/mutation! data)]
;; (th/print-result! out)
(let [mock (deref mock)
[_ _ params] (:call-args mock)]
;; (clojure.pprint/pprint params)
(t/is (:called? mock))
(t/is (= (:email data) (:to params)))
(t/is (contains? params :extra-data))
(t/is (contains? params :token)))
(let [result (:result out)]
(t/is (false? (:is-demo result)))
(t/is (= (:email data) (:email result)))
(t/is (= "penpot" (:auth-backend result)))
(t/is (= "foobar" (:fullname result)))
(t/is (not (contains? result :password)))))))
(t/deftest test-register-profile-with-bounced-email
(with-mocks [mock {:target 'app.emails/send!
:return nil}]
(let [pool (:app.db/pool th/*system*)
data {::th/type :register-profile
:email "user@example.com"
:password "foobar"
:fullname "foobar"}
_ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
out (th/mutation! data)]
;; (th/print-result! out)
(let [mock (deref mock)]
(t/is (false? (:called? mock))))
(let [error (:error out)
edata (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type edata) :validation))
(t/is (= (:code edata) :email-has-permanent-bounces))))))
(t/deftest test-register-profile-with-complained-email
(with-mocks [mock {:target 'app.emails/send!
:return nil}]
(let [pool (:app.db/pool th/*system*)
data {::th/type :register-profile
:email "user@example.com"
:password "foobar"
:fullname "foobar"}
_ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
out (th/mutation! data)]
(let [mock (deref mock)]
(t/is (true? (:called? mock))))
(let [result (:result out)]
(t/is (= (:email data) (:email result)))))))

View file

@ -122,18 +122,6 @@
(seq params)) (seq params))
(send-mutation! id form))) (send-mutation! id form)))
(defmethod mutation :login
[id params]
(let [uri (str cfg/public-uri "/api/login")]
(->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response))))
(defmethod mutation :logout
[id params]
(let [uri (str cfg/public-uri "/api/logout")]
(->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response))))
(defmethod mutation :login-with-ldap (defmethod mutation :login-with-ldap
[id params] [id params]
(let [uri (str cfg/public-uri "/api/login-ldap")] (let [uri (str cfg/public-uri "/api/login-ldap")]

View file

@ -63,7 +63,6 @@
on-error on-error
(fn [form event] (fn [form event]
(js/console.log error?)
(reset! error? true)) (reset! error? true))
on-submit on-submit
@ -107,8 +106,7 @@
:help-icon i/eye :help-icon i/eye
:label (tr "auth.password")}]] :label (tr "auth.password")}]]
[:& fm/submit-button [:& fm/submit-button
{:label (tr "auth.login-submit") {:label (tr "auth.login-submit")}]
:on-click on-submit}]
(when cfg/login-with-ldap (when cfg/login-with-ldap
[:& fm/submit-button [:& fm/submit-button