From 860a97a76914fde298a75b7c6e7268d521d77a12 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 7 Jan 2021 12:04:12 +0100 Subject: [PATCH] :bug: Add missing files. --- backend/src/app/http/auth/github.clj | 172 ++++++++++++++++++ .../resources/images/icons/brand-github.svg | 3 + 2 files changed, 175 insertions(+) create mode 100644 backend/src/app/http/auth/github.clj create mode 100644 frontend/resources/images/icons/brand-github.svg diff --git a/backend/src/app/http/auth/github.clj b/backend/src/app/http/auth/github.clj new file mode 100644 index 000000000..091a7155c --- /dev/null +++ b/backend/src/app/http/auth/github.clj @@ -0,0 +1,172 @@ +;; 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-2021 UXBOX Labs SL + +(ns app.http.auth.github + (:require + [app.common.exceptions :as ex] + [app.common.spec :as us] + [app.config :as cfg] + [app.db :as db] + [app.http.session :as session] + [app.util.http :as http] + [app.util.time :as dt] + [clojure.data.json :as json] + [clojure.spec.alpha :as s] + [clojure.tools.logging :as log] + [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 code state] + (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) + :body (u/map->query-string params)} + res (http/send! req)] + + (when (not= 200 (:status res)) + (ex/raise :type :internal + :code :invalid-response-from-github + :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 github access token request" e) + nil)))) + +(defn- get-user-info + [token] + (let [req {:uri (str user-info-url) + :headers {"authorization" (str "token " token)} + :method :get} + res (http/send! req)] + + (when (not= 200 (:status res)) + (ex/raise :type :internal + :code :invalid-response-from-github + :context {:status (:status res) + :body (:body res)})) + + (try + (let [data (json/read-str (:body res))] + {: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 + [{:keys [tokens] :as cfg} request] + (let [state (tokens :generate + {:iss :github-oauth + :exp (dt/in-future "15m")}) + + params {:client_id (:client-id cfg/config) + :redirect_uri (build-redirect-url) + :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 + [{:keys [tokens rpc session] :as cfg} request] + (let [state (get-in request [:params :state]) + _ (tokens :verify {:token state :iss :github-oauth}) + info (some-> (get-in request [:params :code]) + (get-access-token state) + (get-user-info))] + + (when-not info + (ex/raise :type :authentication + :code :unable-to-authenticate-with-github)) + + (let [method-fn (get-in rpc [:method :mutations :login-or-register]) + profile (method-fn {:email (:email info) + :fullname (:fullname info)}) + uagent (get-in request [:headers "user-agent"]) + + token (tokens :generate + {:iss :auth + :exp (dt/in-future "15m") + :profile-id (:id profile)}) + + uri (-> (u/uri (:public-uri cfg/config)) + (assoc :path "/#/auth/verify-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/cookies {:value sid}) + :body ""}))) + +;; --- 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.auth/github [_] + (s/keys :req-un [::public-uri + ::session + ::tokens] + :opt-un [::client-id + ::client-secret])) + +(defn- default-handler + [req] + (ex/raise :type :not-found)) + +(defmethod ig/init-key :app.http.auth/github + [_ cfg] + (if (and (:client-id cfg) + (:client-secret cfg)) + {:auth-handler #(auth cfg %) + :callback-handler #(callback cfg %)} + {:auth-handler default-handler + :callback-handler default-handler})) + diff --git a/frontend/resources/images/icons/brand-github.svg b/frontend/resources/images/icons/brand-github.svg new file mode 100644 index 000000000..91ce17a60 --- /dev/null +++ b/frontend/resources/images/icons/brand-github.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file