mirror of
https://github.com/penpot/penpot.git
synced 2025-05-13 14:56:38 +02:00
🎉 Add generic oauth2/openid-connect authentication subsystem.
This commit is contained in:
parent
9e5923004f
commit
63b95e71a7
17 changed files with 368 additions and 620 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Normal file
278
backend/src/app/http/oauth.clj
Normal file
|
@ -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))
|
|
@ -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}))
|
||||
|
|
@ -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}))
|
|
@ -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}))
|
|
@ -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
|
||||
: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)
|
||||
: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
|
||||
{: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
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
.form-container {
|
||||
width: 412px;
|
||||
|
||||
.btn-ocean {
|
||||
.auth-buttons {
|
||||
margin-top: $x-big;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -29,26 +29,10 @@
|
|||
(s/def ::login-form
|
||||
(s/keys :req-un [::email ::password]))
|
||||
|
||||
(defn- login-with-google
|
||||
[event params]
|
||||
(defn- login-with-oauth
|
||||
[event provider 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]
|
||||
(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
|
||||
|
|
|
@ -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")]])]
|
||||
(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"])
|
||||
[:& login/login-buttons {:params params}]]])
|
||||
|
||||
(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")])])
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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…
Add table
Add a link
Reference in a new issue