THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

🎉 Add generic oauth2/openid-connect authentication subsystem.

pull/847/head
Andrey Antukh 3 weeks ago
committed by Andrés Moya
parent
commit
63b95e71a7
  1. 3
      CHANGES.md
  2. 12
      backend/src/app/config.clj
  3. 11
      backend/src/app/http.clj
  4. 278
      backend/src/app/http/oauth.clj
  5. 157
      backend/src/app/http/oauth/github.clj
  6. 166
      backend/src/app/http/oauth/gitlab.clj
  7. 181
      backend/src/app/http/oauth/google.clj
  8. 33
      backend/src/app/main.clj
  9. 1
      docker/images/files/config.js
  10. 9
      docker/images/files/nginx-entrypoint.sh
  11. 2
      frontend/resources/styles/main/layouts/login.scss
  12. 1
      frontend/src/app/config.cljs
  13. 22
      frontend/src/app/main/repo.cljs
  14. 68
      frontend/src/app/main/ui/auth/login.cljs
  15. 26
      frontend/src/app/main/ui/auth/register.cljs
  16. 12
      frontend/translations/en.po
  17. 12
      frontend/translations/es.po

3
CHANGES.md

@ -8,7 +8,8 @@
- Allow to group assets (components and graphics) [Taiga #1289](https://tree.taiga.io/project/penpot/us/1289)
- Internal: refactor of http client, replace internal xhr usage with more modern Fetch API.
- New features for paths: snap points on edition, add/remove nodes, merge/join/split nodes. [Taiga #907](https://tree.taiga.io/project/penpot/us/907)
- Add OpenID-Connect support.
- Reimplement social auth providers on top of the generic openid impl.
### :bug: Bugs fixed

12
backend/src/app/config.clj

@ -105,6 +105,12 @@
(s/def ::gitlab-client-secret ::us/string)
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::oidc-client-id ::us/string)
(s/def ::oidc-client-secret ::us/string)
(s/def ::oidc-base-uri ::us/string)
(s/def ::oidc-token-uri ::us/string)
(s/def ::oidc-auth-uri ::us/string)
(s/def ::oidc-user-uri ::us/string)
(s/def ::host ::us/string)
(s/def ::http-server-port ::us/integer)
(s/def ::http-session-cookie-name ::us/string)
@ -178,6 +184,12 @@
::gitlab-client-secret
::google-client-id
::google-client-secret
::oidc-client-id
::oidc-client-secret
::oidc-base-uri
::oidc-token-uri
::oidc-auth-uri
::oidc-user-uri
::host
::http-server-port
::http-session-idle-max-age

11
backend/src/app/http.clj

@ -149,15 +149,8 @@
["/feedback" {:middleware [(:middleware session)]
:post feedback}]
["/oauth"
["/google" {:post (get-in oauth [:google :handler])}]
["/google/callback" {:get (get-in oauth [:google :callback-handler])}]
["/gitlab" {:post (get-in oauth [:gitlab :handler])}]
["/gitlab/callback" {:get (get-in oauth [:gitlab :callback-handler])}]
["/github" {:post (get-in oauth [:github :handler])}]
["/github/callback" {:get (get-in oauth [:github :callback-handler])}]]
["/auth/oauth/:provider" {:post (:handler oauth)}]
["/auth/oauth/:provider/callback" {:get (:callback-handler oauth)}]
["/rpc" {:middleware [(:middleware session)
middleware/activity-logger]}

278
backend/src/app/http/oauth.clj

@ -0,0 +1,278 @@
;; 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/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.oauth
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cf]
[app.util.http :as http]
[app.util.logging :as l]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[lambdaisland.uri :as u]))
(defn redirect-response
[uri]
{:status 302
:headers {"location" (str uri)}
:body ""})
(defn generate-error-redirect-uri
[cfg]
(-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth"}))))
(defn register-profile
[{:keys [rpc] :as cfg} info]
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info)
:backend (:backend info)
:fullname (:fullname info)})]
(cond-> profile
(some? (:invitation-token info))
(assoc :invitation-token (:invitation-token info)))))
(defn generate-redirect-uri
[{:keys [tokens] :as cfg} profile]
(let [token (or (:invitation-token profile)
(tokens :generate {:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)}))]
(-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string {:token token})))))
(defn- build-redirect-uri
[{:keys [provider] :as cfg}]
(let [public (u/uri (:public-uri cfg))]
(str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback")))))
(defn- build-auth-uri
[{:keys [provider] :as cfg} state]
(let [params {:client_id (:client-id provider)
:redirect_uri (build-redirect-uri cfg)
:response_type "code"
:state state
:scope (:scope provider)}
query (u/map->query-string params)]
(-> (u/uri (:auth-uri provider))
(assoc :query query)
(str))))
(defn retrieve-access-token
[{:keys [provider] :as cfg} code]
(try
(let [params {:client_id (:client-id provider)
:client_secret (:client-secret provider)
:code code
:grant_type "authorization_code"
:redirect_uri (build-redirect-uri cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri (:token-uri provider)
:body (u/map->query-string params)}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:token (get data "access_token")
:type (get data "token_type")})))
(catch Exception e
(l/error :hint "unexpected error on retrieve-access-token"
:cause e)
nil)))
(defn- retrieve-user-info
[{:keys [provider] :as cfg} tdata]
(try
(let [req {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:email (get data "email")
:backend (:name provider)
:fullname (get data "name")})))
(catch Exception e
(l/error :hint "unexpected exception on retrieve-user-info"
:cause e)
nil)))
(defn retrieve-info
[{:keys [tokens] :as cfg} request]
(let [state (get-in request [:params :state])
state (tokens :verify {:token state :iss :oauth})
info (some->> (get-in request [:params :code])
(retrieve-access-token cfg)
(retrieve-user-info cfg))]
(when-not info
(ex/raise :type :internal
:code :unable-to-auth))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state)))))
;; --- HTTP HANDLERS
(defn- auth-handler
[{:keys [tokens] :as cfg} request]
(let [invitation (get-in request [:params :invitation-token])
state (tokens :generate
{:iss :oauth
:invitation-token invitation
:exp (dt/in-future "15m")})
uri (build-auth-uri cfg state)]
{:status 200
:body {:redirect-uri uri}}))
(defn- callback-handler
[{:keys [session] :as cfg} request]
(try
(let [info (retrieve-info cfg request)
profile (register-profile cfg info)
uri (generate-redirect-uri cfg profile)
sxf ((:create session) (:id profile))]
(->> (redirect-response uri)
(sxf request)))
(catch Exception _e
(-> (generate-error-redirect-uri cfg)
(redirect-response)))))
;; --- INIT
(declare initialize)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::tokens fn?)
(s/def ::rpc map?)
(defmethod ig/pre-init-spec :app.http.oauth/handlers [_]
(s/keys :req-un [::public-uri ::session ::tokens ::rpc]))
(defn wrap-handler
[cfg handler]
(fn [request]
(let [provider (get-in request [:path-params :provider])
provider (get-in @cfg [:providers provider])]
(when-not provider
(ex/raise :type :not-found
:context {:provider provider}
:hint "provider not configured"))
(-> (assoc @cfg :provider provider)
(handler request)))))
(defmethod ig/init-key :app.http.oauth/handlers
[_ cfg]
(let [cfg (initialize cfg)]
{:handler (wrap-handler cfg auth-handler)
:callback-handler (wrap-handler cfg callback-handler)}))
(defn- discover-oidc-config
[{:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (http/send! {:method :get :uri (str discovery-uri)})]
(when (= 200 (:status response))
(let [data (json/read-str (:body response))]
(assoc opts
:token-uri (get data "token_endpoint")
:auth-uri (get data "authorization_endpoint")
:user-uri (get data "userinfo_endpoint"))))))
(defn- initialize-oidc-provider
[cfg]
(let [opts {:base-uri (cf/get :oidc-base-uri)
:client-id (cf/get :oidc-client-id)
:client-secret (cf/get :oidc-client-secret)
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:scope "openid profile email name"
:name "oidc"}]
(if (and (string? (:base-uri opts))
(string? (:client-id opts))
(string? (:client-secret opts)))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
(do
(l/info :action "initialize" :provider "oid" :method "static")
(assoc-in cfg [:providers "oidc"] opts))
(let [opts (discover-oidc-config opts)]
(l/info :action "initialize" :provider "oid" :method "discover")
(assoc-in cfg [:providers "oidc"] opts)))
cfg)))
(defn- initialize-google-provider
[cfg]
(let [opts {:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)
:scope (str "email profile "
"https://www.googleapis.com/auth/userinfo.email "
"https://www.googleapis.com/auth/userinfo.profile "
"openid")
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
:name "google"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "google")
(assoc-in cfg [:providers "google"] opts))
cfg)))
(defn- initialize-github-provider
[cfg]
(let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)
:scope "user:email"
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:user-uri "https://api.github.com/user"
:name "github"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "github")
(assoc-in cfg [:providers "github"] opts))
cfg)))
(defn- initialize-gitlab-provider
[cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)
:scope "read_user"
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/api/v4/user")
:name "gitlab"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "gitlab")
(assoc-in cfg [:providers "gitlab"] opts))
cfg)))
(defn- initialize
[cfg]
(let [cfg (agent cfg :error-mode :continue)]
(send-off cfg initialize-google-provider)
(send-off cfg initialize-gitlab-provider)
(send-off cfg initialize-github-provider)
(send-off cfg initialize-oidc-provider)
cfg))

157
backend/src/app/http/oauth/github.clj

@ -1,157 +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/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.oauth.github
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.http.oauth.google :as gg]
[app.util.http :as http]
[app.util.logging :as l]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[lambdaisland.uri :as u]))
(def base-github-uri
(u/uri "https://github.com"))
(def base-api-github-uri
(u/uri "https://api.github.com"))
(def authorize-uri
(assoc base-github-uri :path "/login/oauth/authorize"))
(def token-url
(assoc base-github-uri :path "/login/oauth/access_token"))
(def user-info-url
(assoc base-api-github-uri :path "/user"))
(def scope "user:email")
(defn- build-redirect-url
[cfg]
(let [public (u/uri (:public-uri cfg))]
(str (assoc public :path "/api/oauth/github/callback"))))
(defn- get-access-token
[cfg state code]
(try
(let [params {:client_id (:client-id cfg)
:client_secret (:client-secret cfg)
:code code
:state state
:redirect_uri (build-redirect-url cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"
"accept" "application/json"}
:uri (str token-url)
:timeout 6000
:body (u/map->query-string params)}
res (http/send! req)]
(when (= 200 (:status res))
(-> (json/read-str (:body res))
(get "access_token"))))
(catch Exception e
(l/error :hint "unexpected error on get-access-token"
:cause e)
nil)))
(defn- get-user-info
[_ token]
(try
(let [req {:uri (str user-info-url)
:headers {"authorization" (str "token " token)}
:timeout 6000
:method :get}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:email (get data "email")
:backend "github"
:fullname (get data "name")})))
(catch Exception e
(l/error :hint "unexpected exception on get-user-info"
:cause e)
nil)))
(defn- retrieve-info
[{:keys [tokens] :as cfg} request]
(let [token (get-in request [:params :state])
state (tokens :verify {:token token :iss :github-oauth})
info (some->> (get-in request [:params :code])
(get-access-token cfg state)
(get-user-info cfg))]
(when-not info
(ex/raise :type :internal
:code :unable-to-auth))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state)))))
(defn auth-handler
[{:keys [tokens] :as cfg} request]
(let [invitation (get-in request [:params :invitation-token])
state (tokens :generate {:iss :github-oauth
:invitation-token invitation
:exp (dt/in-future "15m")})
params {:client_id (:client-id cfg)
:redirect_uri (build-redirect-url cfg)
:state state
:scope scope}
query (u/map->query-string params)
uri (-> authorize-uri
(assoc :query query))]
{:status 200
:body {:redirect-uri (str uri)}}))
(defn- callback-handler
[{:keys [session] :as cfg} request]
(try
(let [info (retrieve-info cfg request)
profile (gg/register-profile cfg info)
uri (gg/generate-redirect-uri cfg profile)
sxf ((:create session) (:id profile))]
(->> (gg/redirect-response uri)
(sxf request)))
(catch Exception _e
(-> (gg/generate-error-redirect-uri cfg)
(gg/redirect-response)))))
;; --- ENTRY POINT
(s/def ::client-id ::us/not-empty-string)
(s/def ::client-secret ::us/not-empty-string)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::tokens fn?)
(defmethod ig/pre-init-spec :app.http.oauth/github [_]
(s/keys :req-un [::public-uri
::session
::tokens]
:opt-un [::client-id
::client-secret]))
(defn- default-handler
[_]
(ex/raise :type :not-found))
(defmethod ig/init-key :app.http.oauth/github
[_ cfg]
(if (and (:client-id cfg)
(:client-secret cfg))
{:handler #(auth-handler cfg %)
:callback-handler #(callback-handler cfg %)}
{:handler default-handler
:callback-handler default-handler}))

166
backend/src/app/http/oauth/gitlab.clj

@ -1,166 +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/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.oauth.gitlab
(:require
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.http.oauth.google :as gg]
[app.util.http :as http]
[app.util.logging :as l]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[lambdaisland.uri :as u]))
(def scope "read_user")
(defn- build-redirect-url
[cfg]
(let [public (u/uri (:public-uri cfg))]
(str (assoc public :path "/api/oauth/gitlab/callback"))))
(defn- build-oauth-uri
[cfg]
(let [base-uri (u/uri (:base-uri cfg))]
(assoc base-uri :path "/oauth/authorize")))
(defn- build-token-url
[cfg]
(let [base-uri (u/uri (:base-uri cfg))]
(str (assoc base-uri :path "/oauth/token"))))
(defn- build-user-info-url
[cfg]
(let [base-uri (u/uri (:base-uri cfg))]
(str (assoc base-uri :path "/api/v4/user"))))
(defn- get-access-token
[cfg code]
(try
(let [params {:client_id (:client-id cfg)
:client_secret (:client-secret cfg)
:code code
:grant_type "authorization_code"
:redirect_uri (build-redirect-url cfg)}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri (build-token-url cfg)
:body (u/map->query-string params)}
res (http/send! req)]
(when (= 200 (:status res))
(-> (json/read-str (:body res))
(get "access_token"))))
(catch Exception e
(l/error :hint "unexpected error on get-access-token"
:cause e)
nil)))
(defn- get-user-info
[cfg token]
(try
(let [req {:uri (build-user-info-url cfg)
:headers {"Authorization" (str "Bearer " token)}
:timeout 6000
:method :get}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:email (get data "email")
:backend "gitlab"
:fullname (get data "name")})))
(catch Exception e
(l/error :hint "unexpected exception on get-user-info"
:cause e)
nil)))
(defn- retrieve-info
[{:keys [tokens] :as cfg} request]
(let [token (get-in request [:params :state])
state (tokens :verify {:token token :iss :gitlab-oauth})
info (some->> (get-in request [:params :code])
(get-access-token cfg)
(get-user-info cfg))]
(when-not info
(ex/raise :type :internal
:code :unable-to-auth))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state)))))
(defn- auth-handler
[{:keys [tokens] :as cfg} request]
(let [invitation (get-in request [:params :invitation-token])
state (tokens :generate
{:iss :gitlab-oauth
:invitation-token invitation
:exp (dt/in-future "15m")})
params {:client_id (:client-id cfg)
:redirect_uri (build-redirect-url cfg)
:response_type "code"
:state state
:scope scope}
query (u/map->query-string params)
uri (-> (build-oauth-uri cfg)
(assoc :query query))]
{:status 200
:body {:redirect-uri (str uri)}}))
(defn- callback-handler
[{:keys [session] :as cfg} request]
(try
(let [info (retrieve-info cfg request)
profile (gg/register-profile cfg info)
uri (gg/generate-redirect-uri cfg profile)
sxf ((:create session) (:id profile))]
(->> (gg/redirect-response uri)
(sxf request)))
(catch Exception _e
(-> (gg/generate-error-redirect-uri cfg)
(gg/redirect-response)))))
(s/def ::client-id ::us/not-empty-string)
(s/def ::client-secret ::us/not-empty-string)
(s/def ::base-uri ::us/not-empty-string)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::tokens fn?)
(defmethod ig/pre-init-spec :app.http.oauth/gitlab [_]
(s/keys :req-un [::public-uri
::session
::tokens]
:opt-un [::base-uri
::client-id
::client-secret]))
(defmethod ig/prep-key :app.http.oauth/gitlab
[_ cfg]
(d/merge {:base-uri "https://gitlab.com"}
(d/without-nils cfg)))
(defn- default-handler
[_]
(ex/raise :type :not-found))
(defmethod ig/init-key :app.http.oauth/gitlab
[_ cfg]
(if (and (:client-id cfg)
(:client-secret cfg))
{:handler #(auth-handler cfg %)
:callback-handler #(callback-handler cfg %)}
{:handler default-handler
:callback-handler default-handler}))

181
backend/src/app/http/oauth/google.clj

@ -1,181 +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/.
;;
;; Copyright (c) UXBOX Labs SL
(ns app.http.oauth.google
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.util.http :as http]
[app.util.logging :as l]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.spec.alpha :as s]
[integrant.core :as ig]
[lambdaisland.uri :as u]))
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
(def scope
(str "email profile "
"https://www.googleapis.com/auth/userinfo.email "
"https://www.googleapis.com/auth/userinfo.profile "
"openid"))
(defn- build-redirect-url
[cfg]
(let [public (u/uri (:public-uri cfg))]
(str (assoc public :path "/api/oauth/google/callback"))))
(defn- get-access-token
[cfg code]
(try
(let [params {:code code
:client_id (:client-id cfg)
:client_secret (:client-secret cfg)
:redirect_uri (build-redirect-url cfg)
:grant_type "authorization_code"}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri "https://oauth2.googleapis.com/token"
:timeout 6000
:body (u/map->query-string params)}
res (http/send! req)]
(when (= 200 (:status res))
(-> (json/read-str (:body res))
(get "access_token"))))
(catch Exception e
(l/error :hint "unexpected error on get-access-token"
:cause e)
nil)))
(defn- get-user-info
[_ token]
(try
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
:headers {"Authorization" (str "Bearer " token)}
:timeout 6000
:method :get}
res (http/send! req)]
(when (= 200 (:status res))
(let [data (json/read-str (:body res))]
{:email (get data "email")
:backend "google"
:fullname (get data "name")})))
(catch Exception e
(l/error :hint "unexpected exception on get-user-info"
:cause e)
nil)))
(defn- retrieve-info
[{:keys [tokens] :as cfg} request]
(let [token (get-in request [:params :state])
state (tokens :verify {:token token :iss :google-oauth})
info (some->> (get-in request [:params :code])
(get-access-token cfg)
(get-user-info cfg))]
(when-not info
(ex/raise :type :internal
:code :unable-to-auth))
(cond-> info
(some? (:invitation-token state))
(assoc :invitation-token (:invitation-token state)))))
(defn register-profile
[{:keys [rpc] :as cfg} info]
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info)
:backend (:backend info)
:fullname (:fullname info)})]
(cond-> profile
(some? (:invitation-token info))
(assoc :invitation-token (:invitation-token info)))))
(defn generate-redirect-uri
[{:keys [tokens] :as cfg} profile]
(let [token (or (:invitation-token profile)
(tokens :generate {:iss :auth
:exp (dt/in-future "15m")
:profile-id (:id profile)}))]
(-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/verify-token")
(assoc :query (u/map->query-string {:token token})))))
(defn generate-error-redirect-uri
[cfg]
(-> (u/uri (:public-uri cfg))
(assoc :path "/#/auth/login")
(assoc :query (u/map->query-string {:error "unable-to-auth"}))))
(defn redirect-response
[uri]
{:status 302
:headers {"location" (str uri)}
:body ""})
(defn- auth-handler
[{:keys [tokens] :as cfg} request]
(let [invitation (get-in request [:params :invitation-token])
state (tokens :generate
{:iss :google-oauth
:invitation-token invitation
:exp (dt/in-future "15m")})
params {:scope scope
:access_type "offline"
:include_granted_scopes true
:state state
:response_type "code"
:redirect_uri (build-redirect-url cfg)
:client_id (:client-id cfg)}
query (u/map->query-string params)
uri (-> (u/uri base-goauth-uri)
(assoc :query query))]
{:status 200
:body {:redirect-uri (str uri)}}))
(defn- callback-handler
[{:keys [session] :as cfg} request]
(try
(let [info (retrieve-info cfg request)
profile (register-profile cfg info)
uri (generate-redirect-uri cfg profile)
sxf ((:create session) (:id profile))]
(->> (redirect-response uri)
(sxf request)))
(catch Exception _e
(-> (generate-error-redirect-uri cfg)
(redirect-response)))))
(s/def ::client-id ::us/not-empty-string)
(s/def ::client-secret ::us/not-empty-string)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::tokens fn?)
(defmethod ig/pre-init-spec :app.http.oauth/google [_]
(s/keys :req-un [::public-uri
::session
::tokens]
:opt-un [::client-id
::client-secret]))
(defn- default-handler
[_]
(ex/raise :type :not-found))
(defmethod ig/init-key :app.http.oauth/google
[_ cfg]
(if (and (:client-id cfg)
(:client-secret cfg))
{:handler #(auth-handler cfg %)
:callback-handler #(callback-handler cfg %)}
{:handler default-handler
:callback-handler default-handler}))

33
backend/src/app/main.clj

@ -86,13 +86,12 @@
:ws {"/ws/notifications" (ig/ref :app.notifications/handler)}}
:app.http/router
{
:rpc (ig/ref :app.rpc/rpc)
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens)
:public-uri (cf/get :public-uri)
:metrics (ig/ref :app.metrics/metrics)
:oauth (ig/ref :app.http.oauth/all)
:oauth (ig/ref :app.http.oauth/handlers)
:assets (ig/ref :app.http.assets/handlers)
:storage (ig/ref :app.storage/storage)
:sns-webhook (ig/ref :app.http.awsns/handler)
@ -109,35 +108,11 @@
:app.http.feedback/handler
{:pool (ig/ref :app.db/pool)}
:app.http.oauth/all
{:google (ig/ref :app.http.oauth/google)
:gitlab (ig/ref :app.http.oauth/gitlab)
:github (ig/ref :app.http.oauth/github)}
:app.http.oauth/google
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens)
:public-uri (cf/get :public-uri)
:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)}
:app.http.oauth/github
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens)
:public-uri (cf/get :public-uri)
:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)}
:app.http.oauth/gitlab
:app.http.oauth/handlers
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens)
:public-uri (cf/get :public-uri)
:base-uri (cf/get :gitlab-base-uri)
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)}
:public-uri (cf/get :public-uri)}
;; RLimit definition for password hashing
:app.rlimits/password

1
docker/images/files/config.js

@ -6,5 +6,6 @@
//var penpotGoogleClientID = "<google-client-id-here>";
//var penpotGitlabClientID = "<gitlab-client-id-here>";
//var penpotGithubClientID = "<github-client-id-here>";
//var penpotOIDCClientID = "<oidc-client-id-here>";
//var penpotLoginWithLDAP = <true|false>;
//var penpotRegistrationEnabled = <true|false>;

9
docker/images/files/nginx-entrypoint.sh

@ -69,6 +69,14 @@ update_github_client_id() {
fi
}
update_oidc_client_id() {
if [ -n "$PENPOT_OIDC_CLIENT_ID" ]; then
log "Updating Oidc Client Id: $PENPOT_OIDC_CLIENT_ID"
sed -i \
-e "s|^//var penpotOIDCClientID = \".*\";|var penpotOIDCClientID = \"$PENPOT_OIDC_CLIENT_ID\";|g" \
"$1"
fi
}
update_login_with_ldap() {
if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then
@ -95,6 +103,7 @@ update_allow_demo_users /var/www/app/js/config.js
update_google_client_id /var/www/app/js/config.js
update_gitlab_client_id /var/www/app/js/config.js
update_github_client_id /var/www/app/js/config.js
update_oidc_client_id /var/www/app/js/config.js
update_login_with_ldap /var/www/app/js/config.js
update_registration_enabled /var/www/app/js/config.js

2
frontend/resources/styles/main/layouts/login.scss

@ -57,7 +57,7 @@
.form-container {
width: 412px;
.btn-ocean {
.auth-buttons {
margin-top: $x-big;
}

1
frontend/src/app/config.cljs

@ -69,6 +69,7 @@
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
(def github-client-id (obj/get global "penpotGithubClientID" nil))
(def oidc-client-id (obj/get global "penpotOIDCClientID" nil))
(def login-with-ldap (obj/get global "penpotLoginWithLDAP" false))
(def registration-enabled (obj/get global "penpotRegistrationEnabled" true))
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))

22
frontend/src/app/main/repo.cljs

@ -6,6 +6,7 @@
(ns app.main.repo
(:require
[app.common.data :as d]
[beicon.core :as rx]
[lambdaisland.uri :as u]
[cuerdas.core :as str]
@ -84,23 +85,10 @@
([id] (mutation id {}))
([id params] (mutation id params)))
(defmethod mutation :login-with-google
[id params]
(let [uri (u/join base-uri "api/oauth/google")]
(->> (http/send! {:method :post :uri uri :query params})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
(defmethod mutation :login-with-gitlab
[id params]
(let [uri (u/join base-uri "api/oauth/gitlab")]
(->> (http/send! {:method :post :uri uri :query params})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
(defmethod mutation :login-with-github
[id params]
(let [uri (u/join base-uri "api/oauth/github")]
(defmethod mutation :login-with-oauth
[id {:keys [provider] :as params}]
(let [uri (u/join base-uri "api/auth/oauth/" (d/name provider))
params (dissoc params :provider)]
(->> (http/send! {:method :post :uri uri :query params})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))

68
frontend/src/app/main/ui/auth/login.cljs

@ -29,26 +29,10 @@
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- login-with-google
[event params]
(dom/prevent-default event)
(->> (rp/mutation! :login-with-google params)
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(.replace js/location redirect-uri))
(fn [{:keys [type] :as error}]
(st/emit! (dm/error (tr "errors.google-auth-not-enabled")))))))
(defn- login-with-gitlab
[event params]
(dom/prevent-default event)
(->> (rp/mutation! :login-with-gitlab params)
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(.replace js/location redirect-uri)))))
(defn- login-with-github
[event params]
(defn- login-with-oauth
[event provider params]
(dom/prevent-default event)
(->> (rp/mutation! :login-with-github params)
(->> (rp/mutation! :login-with-oauth (assoc params :provider provider))
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(.replace js/location redirect-uri)))))
@ -127,6 +111,33 @@
{:label (tr "auth.login-with-ldap-submit")
:on-click on-submit-ldap}])]]))
(mf/defc login-buttons
[{:keys [params] :as props}]
[:div.auth-buttons
(when cfg/google-client-id
[:a.btn-ocean.btn-large.btn-google-auth
{:on-click #(login-with-oauth % :google params)}
(tr "auth.login-with-google-submit")])
(when cfg/gitlab-client-id
[:a.btn-ocean.btn-large.btn-gitlab-auth
{:on-click #(login-with-oauth % :gitlab params)}
[:img.logo
{:src "/images/icons/brand-gitlab.svg"}]
(tr "auth.login-with-gitlab-submit")])
(when cfg/github-client-id
[:a.btn-ocean.btn-large.btn-github-auth
{:on-click #(login-with-oauth % :github params)}
[:img.logo
{:src "/images/icons/brand-github.svg"}]
(tr "auth.login-with-github-submit")])
(when cfg/oidc-client-id
[:a.btn-ocean.btn-large.btn-github-auth
{:on-click #(login-with-oauth % :oidc params)}
(tr "auth.login-with-oidc-submit")])])
(mf/defc login-page
[{:keys [params] :as props}]
[:div.generic-form.login-form
@ -149,24 +160,7 @@
:tab-index "6"}
(tr "auth.register-submit")]])]
(when cfg/google-client-id
[:a.btn-ocean.btn-large.btn-google-auth
{:on-click #(login-with-google % params)}
"Login with Google"])
(when cfg/gitlab-client-id
[:a.btn-ocean.btn-large.btn-gitlab-auth
{:on-click #(login-with-gitlab % params)}
[:img.logo
{:src "/images/icons/brand-gitlab.svg"}]
(tr "auth.login-with-gitlab-submit")])
(when cfg/github-client-id
[:a.btn-ocean.btn-large.btn-github-auth
{:on-click #(login-with-github % params)}
[:img.logo
{:src "/images/icons/brand-github.svg"}]
(tr "auth.login-with-github-submit")])
[:& login-buttons {:params params}]
(when cfg/allow-demo-users
[:div.links.demo

26
frontend/src/app/main/ui/auth/register.cljs

@ -137,7 +137,6 @@
[:div.notification-text-email (:email params "")]
[:div.notification-text (tr "auth.check-your-email")]])
(mf/defc register-page
[{:keys [params] :as props}]
[:div.form-container
@ -161,24 +160,9 @@
[:span (tr "auth.create-demo-profile") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"}
(tr "auth.create-demo-account")]])]
(when cfg/google-client-id
[:a.btn-ocean.btn-large.btn-google-auth
{:on-click #(login/login-with-google % params)}
"Login with Google"])
(when cfg/gitlab-client-id
[:a.btn-ocean.btn-large.btn-gitlab-auth
{:on-click #(login/login-with-gitlab % params)}
[:img.logo
{:src "/images/icons/brand-gitlab.svg"}]
(tr "auth.login-with-gitlab-submit")])
(when cfg/github-client-id
[:a.btn-ocean.btn-large.btn-github-auth
{:on-click #(login/login-with-github % params)}
[:img.logo
{:src "/images/icons/brand-github.svg"}]
(tr "auth.login-with-github-submit")])])
(tr "auth.create-demo-account")]])
[:& login/login-buttons {:params params}]]])

12
frontend/translations/en.po

@ -64,18 +64,26 @@ msgstr "Enter your details below"
msgid "auth.login-title"
msgstr "Great to see you again!"
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-github-submit"
msgstr "Login with Github"
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-gitlab-submit"
msgstr "Login with Gitlab"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-google-submit"
msgstr "Login with Google"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-ldap-submit"
msgstr "Sign in with LDAP"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-oidc-submit"
msgstr "Login with OpenID (SSO)"
#: src/app/main/ui/auth/recovery.cljs
msgid "auth.new-password"
msgstr "Type a new password"

12
frontend/translations/es.po

@ -60,18 +60,26 @@ msgstr "Introduce tus datos aquí"
msgid "auth.login-title"
msgstr "Encantados de volverte a ver"
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-github-submit"
msgstr "Entrar con Github"
#: src/app/main/ui/auth/register.cljs, src/app/main/ui/auth/login.cljs
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-gitlab-submit"
msgstr "Entrar con Gitlab"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-google-submit"
msgstr "Entrar con Google"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-ldap-submit"
msgstr "Entrar con LDAP"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-oidc-submit"
msgstr "Entrar con OpenID (SSO)"
#: src/app/main/ui/auth/recovery.cljs
msgid "auth.new-password"
msgstr "Introduce la nueva contraseña"

Loading…
Cancel
Save