From cc1353300e3c189d8198d00224febae2839b4ca0 Mon Sep 17 00:00:00 2001 From: Vitaly Kornilov Date: Mon, 24 Aug 2020 13:24:36 +0300 Subject: [PATCH] :sparkles: Login with Gitlab --- backend/src/app/config.clj | 7 + backend/src/app/http.clj | 5 +- backend/src/app/http/auth/gitlab.clj | 153 ++++++++++++++++++ frontend/gulpfile.js | 1 + .../resources/images/icons/brand-gitlab.svg | 10 ++ .../resources/styles/main/layouts/login.scss | 11 ++ frontend/src/app/config.cljs | 1 + frontend/src/app/main/repo.cljs | 6 + frontend/src/app/main/ui/auth/login.cljs | 14 ++ 9 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 backend/src/app/http/auth/gitlab.clj create mode 100644 frontend/resources/images/icons/brand-gitlab.svg diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index eaa0a9218..29c6b3cdf 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -98,6 +98,10 @@ (s/def ::google-client-id ::us/string) (s/def ::google-client-secret ::us/string) +(s/def ::gitlab-client-id ::us/string) +(s/def ::gitlab-client-secret ::us/string) +(s/def ::gitlab-base-uri ::us/string) + (s/def ::ldap-auth-host ::us/string) (s/def ::ldap-auth-port ::us/integer) (s/def ::ldap-bind-dn ::us/string) @@ -118,6 +122,9 @@ ::http-server-port ::google-client-id ::google-client-secret + ::gitlab-client-id + ::gitlab-client-secret + ::gitlab-base-uri ::public-uri ::database-username ::database-password diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index e5afdd856..eed57f905 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -15,6 +15,7 @@ [ring.adapter.jetty9 :as jetty] [app.config :as cfg] [app.http.auth :as auth] + [app.http.auth.gitlab :as gitlab] [app.http.auth.google :as google] [app.http.auth.ldap :as ldap] [app.http.debug :as debug] @@ -40,7 +41,9 @@ ["/oauth" ["/google" {:post google/auth}] - ["/google/callback" {:get google/callback}]] + ["/google/callback" {:get google/callback}] + ["/gitlab" {:post gitlab/auth}] + ["/gitlab/callback" {:get gitlab/callback}]] ["/echo" {:get handlers/echo-handler :post handlers/echo-handler}] diff --git a/backend/src/app/http/auth/gitlab.clj b/backend/src/app/http/auth/gitlab.clj new file mode 100644 index 000000000..c2e8fe828 --- /dev/null +++ b/backend/src/app/http/auth/gitlab.clj @@ -0,0 +1,153 @@ +;; 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.gitlab + (:require + [clojure.data.json :as json] + [clojure.tools.logging :as log] + [lambdaisland.uri :as uri] + [app.common.exceptions :as ex] + [app.config :as cfg] + [app.db :as db] + [app.services.tokens :as tokens] + [app.services.mutations :as sm] + [app.http.session :as session] + [app.util.http :as http])) + + +(def default-base-gitlab-uri "https://gitlab.com") + + +(def scope "read_user") + + +(defn- build-redirect-url + [] + (let [public (uri/uri (:public-uri cfg/config))] + (str (assoc public :path "/api/oauth/gitlab/callback")))) + + +(defn- build-oauth-uri + [] + (let [base-uri (uri/uri (:gitlab-base-uri cfg/config default-base-gitlab-uri))] + (assoc base-uri :path "/oauth/authorize"))) + + +(defn- build-token-url + [] + (let [base-uri (uri/uri (:gitlab-base-uri cfg/config default-base-gitlab-uri))] + (str (assoc base-uri :path "/oauth/token")))) + + +(defn- build-user-info-url + [] + (let [base-uri (uri/uri (:gitlab-base-uri cfg/config default-base-gitlab-uri))] + (str (assoc base-uri :path "/api/v4/user")))) + + +(defn- get-access-token + [code] + (let [params {:client_id (:gitlab-client-id cfg/config) + :client_secret (:gitlab-client-secret cfg/config) + :code code + :grant_type "authorization_code" + :redirect_uri (build-redirect-url)} + req {:method :post + :headers {"content-type" "application/x-www-form-urlencoded"} + :uri (build-token-url) + :body (uri/map->query-string params)} + res (http/send! req)] + + (when (not= 200 (:status res)) + (ex/raise :type :internal + :code :invalid-response-from-gitlab + :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 tooken request" e) + nil)))) + + +(defn- get-user-info + [token] + (let [req {:uri (build-user-info-url) + :headers {"Authorization" (str "Bearer " token)} + :method :get} + res (http/send! req)] + + (when (not= 200 (:status res)) + (ex/raise :type :internal + :code :invalid-response-from-gitlab + :context {:status (:status res) + :body (:body res)})) + + (try + (let [data (json/read-str (:body res))] + ;; (clojure.pprint/pprint data) + {:email (get data "email") + :fullname (get data "name")}) + (catch Throwable e + (log/error "unexpected error on parsing response body from gitlab access tooken request" e) + nil)))) + + +(defn auth + [req] + (let [token (tokens/create! db/pool {:type :gitlab-oauth}) + params {:client_id (:gitlab-client-id cfg/config) + :redirect_uri (build-redirect-url) + :response_type "code" + :state token + :scope scope} + query (uri/map->query-string params) + uri (-> (build-oauth-uri) + (assoc :query query))] + {:status 200 + :body {:redirect-uri (str uri)}})) + + +(defn callback + [req] + (let [token (get-in req [:params :state]) + tdata (tokens/retrieve db/pool token) + info (some-> (get-in req [:params :code]) + (get-access-token) + (get-user-info))] + + (when (not= :gitlab-oauth (:type tdata)) + (ex/raise :type :validation + :code ::tokens/invalid-token)) + + (when-not info + (ex/raise :type :authentication + :code ::unable-to-authenticate-with-gitlab)) + + (let [profile (sm/handle {::sm/type :login-or-register + :email (:email info) + :fullname (:fullname info)}) + uagent (get-in req [:headers "user-agent"]) + + tdata {:type :authentication + :profile profile} + token (tokens/create! db/pool tdata {:valid {:minutes 10}}) + + uri (-> (uri/uri (:public-uri cfg/config)) + (assoc :path "/#/auth/verify-token") + (assoc :query (uri/map->query-string {:token token}))) + sid (session/create (:id profile) uagent)] + + {:status 302 + :headers {"location" (str uri)} + :cookies (session/cookies sid) + :body ""}))) + diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index fa32fc84e..f595e8e3a 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -163,6 +163,7 @@ function templatePipeline(options) { "var appLoginWithLDAP = null;", "var appPublicURI = null;", "var appGoogleClientID = null;", + "var appGitlabClientID = null;", "var appDeployDate = null;", "var appDeployCommit = null;" ]; diff --git a/frontend/resources/images/icons/brand-gitlab.svg b/frontend/resources/images/icons/brand-gitlab.svg new file mode 100644 index 000000000..8dc42e815 --- /dev/null +++ b/frontend/resources/images/icons/brand-gitlab.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index 7d5b25c01..e8d89e846 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -63,4 +63,15 @@ margin-bottom: $medium; text-decoration: none; } + + .btn-gitlab-auth { + margin-bottom: $medium; + text-decoration: none; + + .logo { + width: 20px; + height: 20px; + margin-right: 1rem; + } + } } diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index b593919a6..e882e54b8 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -14,6 +14,7 @@ (def default-language "en") (def demo-warning (obj/get global "appDemoWarning" false)) (def google-client-id (obj/get global "appGoogleClientID" nil)) + (def gitlab-client-id (obj/get global "appGitlabClientID" nil)) (def login-with-ldap (obj/get global "appLoginWithLDAP" false)) (def worker-uri (obj/get global "appWorkerURI" "/js/worker.js")) (def public-uri (or (obj/get global "appPublicURI") diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 2444ec95b..6a051de41 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -71,6 +71,12 @@ (->> (http/send! {:method :post :uri uri}) (rx/mapcat handle-response)))) +(defmethod mutation :login-with-gitlab + [id params] + (let [uri (str cfg/public-uri "/api/oauth/gitlab")] + (->> (http/send! {:method :post :uri uri}) + (rx/mapcat handle-response)))) + (defmethod mutation :upload-media-object [id params] (let [form (js/FormData.)] diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index 2679b83d9..af7e1fd9e 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -40,6 +40,13 @@ (rx/subs (fn [{:keys [redirect-uri] :as rsp}] (.replace js/location redirect-uri))))) +(defn- login-with-gitlab + [event] + (dom/prevent-default event) + (->> (rp/mutation! :login-with-gitlab {}) + (rx/subs (fn [{:keys [redirect-uri] :as rsp}] + (.replace js/location redirect-uri))))) + (mf/defc login-form [{:keys [locale] :as props}] (let [error? (mf/use-state false) @@ -113,6 +120,13 @@ {:on-click login-with-google} "Login with Google"]) + (when cfg/gitlab-client-id + [:a.btn-ocean.btn-large.btn-gitlab-auth + {:on-click login-with-gitlab} + [:img.logo + {:src "/images/icons/brand-gitlab.svg"}] + "Login with Gitlab"]) + [:div.links.demo [:div.link-entry [:span (t locale "auth.create-demo-profile-label") " "]