<commit_before>(ns wombats.handlers.auth
  (:require [clojure.string :refer [ends-with? join split]]
            [taoensso.timbre :as log]
            [io.pedestal.interceptor.helpers :as interceptor]
            [cemerick.url :refer [url-encode url-decode]]
            [org.httpkit.client :as http]
            [cheshire.core :refer [parse-string]]
            [buddy.core.mac :as mac]
            [buddy.core.codecs :as codecs]
            [wombats.interceptors.github :refer [get-github-settings]]
            [wombats.interceptors.authorization :refer [get-api-uri get-hashing-secret]]
            [wombats.daos.helpers :as dao]
            [wombats.constants :refer [github-access-token-url
                                       github-user-profile-url
                                       github-authorize-url
                                       github-scopes]]))

;; Helper Functions

(defn- get-access-token
  "POST request to get a GitHub OAuth token.
  If no auth token is found, nil will return"
  [query-params]
  (http/post github-access-token-url
             {:query-params query-params
              :headers {"Accept" "application/json"}}
             (fn [{:keys [body]}]
               (get (parse-string body true) :access_token nil))))

(defn- get-github-user
  "Fetches the github user attached to a given access token"
  [access-token]
  (http/get github-user-profile-url
            {:headers {"Authorization" (str "token " access-token)
                       "Accept" "application/json"}}))

(defn- remove-slash
  "Removes the trailing slash from a string (if it exists)"
  [string]
  (if (and string
           (ends-with? string "/"))
    (-> string
        (split #"")
        (drop-last)
        (join))
    string))

(defn- get-referer
  "Pulls out the referer from a request object"
  [request]
  (get-in request [:headers "referer"]))

(defn- get-formatted-referer
  "Formats the referer"
  [request]
  (remove-slash (get-referer request)))

(defn- parse-user-response
  "Parses the body if the request succeeded"
  [{:keys [body status]}]
  (when (= status 200)
    (parse-string body true)))

(defn- redirect-home
  "Final redirect home with or without access token appended"
  ([context redirect]
   (assoc context :response (assoc (:response context)
                                   :headers {"Location" redirect}
                                   :status 302)))
  ([context redirect access-token]
   (assoc context :response (assoc (:response context)
                                   :headers {"Location" (str redirect "?access-token=" access-token)}
                                   :status 302))))

;; Handlers

(def ^:swagger-spec signin-spec
  {"/api/v1/auth/github/signin"
   {:get {:description "Gets authorization and access token"
          :tags ["auth"]
          :operationId "signout"
          :response {:302 {:description "signout response"}}}}})

;; TODO signing secret should not be consistant across requests. Gen uuid to send.
(def signin
  "Signin handler"
  (interceptor/before
   ::sign-in
   (fn [{:keys [response request] :as context}]
     (let [{:keys [client-id signing-secret]} (get-github-settings context)
           {access-key :access-key} (:query-params request)
           api-uri (get-api-uri context)
           github-redirect (str github-authorize-url
                                "?client_id=" client-id
                                "&scope=" github-scopes
                                "&state=" (url-encode {:signing-secret signing-secret
                                                       :access-key access-key})
                                "&redirect_uri=" (str api-uri
                                                      "/api/v1/auth/github/callback?referer="
                                                      (get-formatted-referer request)))]

       (assoc context :response (assoc response
                                       :headers {"Location" github-redirect}
                                       :status 302
                                       :body nil))))))

(defn- gen-user-access-token
  "Generates an access token to be used by the wombats client"
  [secret-key github-id]
  (-> (str github-id)
      (mac/hash {:key secret-key :alg :hmac+sha256})
      (codecs/bytes->hex)))

(def github-callback
  "Callback handler from GitHub OAuth request"
  (interceptor/before
   ::github-callback
   (fn [{:keys [request response] :as context}]
     (let [{:keys [client-id
                   client-secret
                   signing-secret]} (get-github-settings context)
           {:keys [code referer state]} (:query-params request)
           create-or-update-user (dao/get-fn :create-or-update-user context)
           get-user-by-github-id (dao/get-fn :get-user-by-github-id context)
           failed-redirect #(redirect-home context (str referer "?login-error=" (url-encode %)))
           {signing-secret-check :signing-secret
            access-key-key :access-key} (read-string (url-decode state))]

       ;; Check to see if the request is originating from the correct user
       (if (= signing-secret-check signing-secret)
         (let [github-access-token @(get-access-token {:client_id client-id
                                                       :client_secret client-secret
                                                       :code code})
               user (when github-access-token
                      (parse-user-response @(get-github-user github-access-token)))]

           ;; Ensure that a github user has been found
           (if (and github-access-token user)
             (let [user-fields (select-keys user [:login :id :avatar_url])
                   user-access-token (gen-user-access-token (get-hashing-secret context)
                                                            (:id user-fields))]

               ;; Update the database with the requesting users information
               (create-or-update-user user-fields
                                      github-access-token
                                      user-access-token
                                      access-key-key)

               (let [updated-user (get-user-by-github-id (:id user-fields))]
                 ;; Check if the user has a vaild access key
                 (if (:user/access-key updated-user)
                   (redirect-home context referer user-access-token)
                   (failed-redirect
                    (str "Oops! What's the magic word?"
                         " Please request an access key from an administrator.")))))
             (failed-redirect "We were unable to lookup your Github Account.")))
         (do
           (log/error (str "Sign in request has been tampored with.\n\n" request))
           (failed-redirect "Something went wrong while trying to sign in.")))))))

(def ^:swagger-spec signout-spec
  {"/api/v1/auth/github/signout"
   {:get {:description "Removes auth token from db and redirects to home."
          :tags ["auth"]
          :operationId "signout"
          :response {:302 {:description "signout response"}}}}})

(def signout
  "Signout handler"
  (interceptor/before
   ::signout
   (fn [{:keys [request response] :as context}]
     (let [access-token (get-in request [:headers "authorization"])
           remove-access-token (dao/get-fn :remove-access-token context)
           web-client-redirect (get-formatted-referer request)]

       (when access-token
         @(remove-access-token access-token))

       (assoc context :response (assoc response
                                       :headers {"Location" web-client-redirect}
                                       :status 302
                                       :body nil))))))
<commit_msg>Add debug logging<commit_after>(ns wombats.handlers.auth
  (:require [clojure.string :refer [ends-with? join split]]
            [taoensso.timbre :as log]
            [io.pedestal.interceptor.helpers :as interceptor]
            [cemerick.url :refer [url-encode url-decode]]
            [org.httpkit.client :as http]
            [cheshire.core :refer [parse-string]]
            [buddy.core.mac :as mac]
            [buddy.core.codecs :as codecs]
            [wombats.interceptors.github :refer [get-github-settings]]
            [wombats.interceptors.authorization :refer [get-api-uri get-hashing-secret]]
            [wombats.daos.helpers :as dao]
            [wombats.constants :refer [github-access-token-url
                                       github-user-profile-url
                                       github-authorize-url
                                       github-scopes]]))

;; Helper Functions

(defn- get-access-token
  "POST request to get a GitHub OAuth token.
  If no auth token is found, nil will return"
  [query-params]
  (http/post github-access-token-url
             {:query-params query-params
              :headers {"Accept" "application/json"}}
             (fn [{:keys [body]}]
               (get (parse-string body true) :access_token nil))))

(defn- get-github-user
  "Fetches the github user attached to a given access token"
  [access-token]
  (http/get github-user-profile-url
            {:headers {"Authorization" (str "token " access-token)
                       "Accept" "application/json"}}))

(defn- remove-slash
  "Removes the trailing slash from a string (if it exists)"
  [string]
  (if (and string
           (ends-with? string "/"))
    (-> string
        (split #"")
        (drop-last)
        (join))
    string))

(defn- get-referer
  "Pulls out the referer from a request object"
  [request]
  (get-in request [:headers "referer"]))

(defn- get-formatted-referer
  "Formats the referer"
  [request]
  (remove-slash (get-referer request)))

(defn- parse-user-response
  "Parses the body if the request succeeded"
  [{:keys [body status]}]
  (when (= status 200)
    (parse-string body true)))

(defn- redirect-home
  "Final redirect home with or without access token appended"
  ([context redirect]
   (assoc context :response (assoc (:response context)
                                   :headers {"Location" redirect}
                                   :status 302)))
  ([context redirect access-token]
   (assoc context :response (assoc (:response context)
                                   :headers {"Location" (str redirect "?access-token=" access-token)}
                                   :status 302))))

;; Handlers

(def ^:swagger-spec signin-spec
  {"/api/v1/auth/github/signin"
   {:get {:description "Gets authorization and access token"
          :tags ["auth"]
          :operationId "signout"
          :response {:302 {:description "signout response"}}}}})

;; TODO signing secret should not be consistant across requests. Gen uuid to send.
(def signin
  "Signin handler"
  (interceptor/before
   ::sign-in
   (fn [{:keys [response request] :as context}]
     (let [{:keys [client-id signing-secret]} (get-github-settings context)
           {access-key :access-key} (:query-params request)
           api-uri (get-api-uri context)
           github-redirect (str github-authorize-url
                                "?client_id=" client-id
                                "&scope=" github-scopes
                                "&state=" (url-encode {:signing-secret signing-secret
                                                       :access-key access-key})
                                "&redirect_uri=" (str api-uri
                                                      "/api/v1/auth/github/callback?referer="
                                                      (get-formatted-referer request)))]

       (assoc context :response (assoc response
                                       :headers {"Location" github-redirect}
                                       :status 302
                                       :body nil))))))

(defn- gen-user-access-token
  "Generates an access token to be used by the wombats client"
  [secret-key github-id]
  (-> (str github-id)
      (mac/hash {:key secret-key :alg :hmac+sha256})
      (codecs/bytes->hex)))

(def github-callback
  "Callback handler from GitHub OAuth request"
  (interceptor/before
   ::github-callback
   (fn [{:keys [request response] :as context}]
     (let [{:keys [client-id
                   client-secret
                   signing-secret]} (get-github-settings context)
           {:keys [code referer state]} (:query-params request)
           create-or-update-user (dao/get-fn :create-or-update-user context)
           get-user-by-github-id (dao/get-fn :get-user-by-github-id context)
           failed-redirect #(redirect-home context (str referer "?login-error=" (url-encode %)))
           {signing-secret-check :signing-secret
            access-key-key :access-key} (read-string (url-decode state))]

       (log/error (str "github-settings: "
                       (get-github-settings context)))

       (log/error (str "request-params: "
                       (:query-params request)))

       (log/error (str "state-decode: "
                       (read-string (url-decode state))))

       ;; Check to see if the request is originating from the correct user
       (if (= signing-secret-check signing-secret)
         (let [github-access-token @(get-access-token {:client_id client-id
                                                       :client_secret client-secret
                                                       :code code})
               user (when github-access-token
                      (parse-user-response @(get-github-user github-access-token)))]

           ;; Ensure that a github user has been found
           (if (and github-access-token user)
             (let [user-fields (select-keys user [:login :id :avatar_url])
                   user-access-token (gen-user-access-token (get-hashing-secret context)
                                                            (:id user-fields))]

               ;; Update the database with the requesting users information
               (create-or-update-user user-fields
                                      github-access-token
                                      user-access-token
                                      access-key-key)

               (let [updated-user (get-user-by-github-id (:id user-fields))]
                 ;; Check if the user has a vaild access key
                 (if (:user/access-key updated-user)
                   (redirect-home context referer user-access-token)
                   (failed-redirect
                    (str "Oops! What's the magic word?"
                         " Please request an access key from an administrator.")))))
             (failed-redirect "We were unable to lookup your Github Account.")))
         (do
           (log/error (str "Sign in request has been tampored with.\n\n" request))
           (failed-redirect "Something went wrong while trying to sign in.")))))))

(def ^:swagger-spec signout-spec
  {"/api/v1/auth/github/signout"
   {:get {:description "Removes auth token from db and redirects to home."
          :tags ["auth"]
          :operationId "signout"
          :response {:302 {:description "signout response"}}}}})

(def signout
  "Signout handler"
  (interceptor/before
   ::signout
   (fn [{:keys [request response] :as context}]
     (let [access-token (get-in request [:headers "authorization"])
           remove-access-token (dao/get-fn :remove-access-token context)
           web-client-redirect (get-formatted-referer request)]

       (when access-token
         @(remove-access-token access-token))

       (assoc context :response (assoc response
                                       :headers {"Location" web-client-redirect}
                                       :status 302
                                       :body nil))))))

{'fraction_non_alphanumeric': 0.11399192530910926, 'fraction_numerical': 0.0034065102195306586, 'mean_word_length': 1.667956916863009, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 56, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '336272', 'n_tokens_mistral': 4362, 'n_tokens_neox': 4160, 'n_words': 1166}