Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hcaptcha to signup form #886

Merged
merged 2 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions resources/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
:client-secret #profile {:production #ssm-parameter "/clojars/production/gitlab_oauth_client_secret"
:default "testing"}
:callback-uri "https://clojars.org/oauth/gitlab/callback"}
:hcaptcha #profile {:production {:site-key #ssm-parameter "/clojars/production/hcaptcha_site_key"
:secret #ssm-parameter "/clojars/production/hcaptcha_secret"}
:default {:site-key "10000000-ffff-ffff-ffff-000000000001"
:secret "0x0000000000000000000000000000000000000000"}}
:index-path "data/index"
:mail #profile {:production {:from "[email protected]"
:hostname "email-smtp.us-east-1.amazonaws.com"
Expand Down
4 changes: 4 additions & 0 deletions src/clojars/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@

(def ^:dynamic *profile* "development")

(defn test-profile?
[]
(= "test" *profile*))

(defmethod aero/reader 'ssm-parameter
[_opts _tag value]
(if (= :production *profile*)
Expand Down
32 changes: 18 additions & 14 deletions src/clojars/friend/registration.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,39 @@
(:require
[cemerick.friend.workflows :as workflow]
[clojars.db :refer [add-user]]
[clojars.hcaptcha :as hcaptcha]
[clojars.http-utils :as http-utils]
[clojars.log :as log]
[clojars.web.user :refer [register-form new-user-validations normalize-email]]
[ring.util.response :refer [response content-type]]
[valip.core :refer [validate]]))

(defn register [db {:keys [email username password confirm]}]
(defn register
[db hcaptcha {:keys [confirm email h-captcha-response password username]}]
(let [email (normalize-email email)]
(log/with-context {:email email
:username username
:tag :registration}
(if-let [errors (apply validate {:email email
:username username
:password password}
(new-user-validations db confirm))]
(if-let [errors (apply validate {:captcha h-captcha-response
:email email
:password password
:username username}
(new-user-validations db hcaptcha confirm))]
(do
(log/info {:status :validation-failed})
(->
(response (register-form {:errors (apply concat (vals errors))
:email email
:username username}
nil))
(content-type "text/html")))
(http-utils/with-extra-csp-srcs
hcaptcha/hcaptcha-csp
(register-form hcaptcha
{:errors (apply concat (vals errors))
:email email
:username username}
nil)))
(do
(add-user db email username password)
(log/info {:status :success})
(workflow/make-auth {:identity username :username username}))))))

(defn workflow [db]
(defn workflow [db hcaptcha]
(fn [{:keys [uri request-method params]}]
(when (and (= "/register" uri)
(= :post request-method))
(register db params))))
(register db hcaptcha params))))
63 changes: 63 additions & 0 deletions src/clojars/hcaptcha.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
(ns clojars.hcaptcha
"See https://docs.hcaptcha.com/#verify-the-user-response-server-side"
(:require
[clojars.log :as log]
[clojars.remote-service :as remote-service :refer [defendpoint]]
[clojure.set :as set]))

(def hcaptcha-csp
(let [hcaptcha-urls ["https://hcaptcha.com" "https://*.hcaptcha.com"]]
{:connect-src hcaptcha-urls
:frame-src hcaptcha-urls
:script-src hcaptcha-urls
:style-src hcaptcha-urls}))

(defrecord Hcaptcha [config])

(defn site-key
[hcaptcha]
(get-in hcaptcha [:config :site-key]))

(defn- secret
[hcaptcha]
(get-in hcaptcha [:config :secret]))

(defendpoint -validate-hcaptcha-response
[_client site-key secret hcaptcha-response]
{:method :post
:url "https://api.hcaptcha.com/siteverify"
:form-params {:response hcaptcha-response
:secret secret
:site-key site-key}})

;; These responses from hcaptcha indicate a misconfiguration or a code bug, so
;; we will throw and error for them.
(def ^:private invalid-error-codes
#{"bad-request"
"invalid-input-secret"
"invalid-or-already-seen-response"
"missing-input-secret"
"sitekey-secret-mismatch"})

(defn- log-and-maybe-thrpw
[{:as _response :keys [error-codes success]}]
(let [log-data {:tag :hcaptcha-response
:error-codes error-codes
:success? success}]
(log/info log-data)
(when (seq (set/intersection invalid-error-codes (set error-codes)))
(throw (ex-info "Invalid hcaptcha configuration" log-data)))))

(defn valid-response?
[hcaptcha hcaptcha-response]
(let [response (-validate-hcaptcha-response (:client hcaptcha)
(site-key hcaptcha)
(secret hcaptcha)
hcaptcha-response)]
(log-and-maybe-thrpw response)
(:success response)))

(defn new-hcaptcha
[config]
(map->Hcaptcha {:config config
:client (remote-service/new-http-remote-service)}))
25 changes: 14 additions & 11 deletions src/clojars/http_utils.clj
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@
(regular-session req)))))

(defn- content-security-policy
[{:as _request ::keys [extra-csp-img-src]}]
[{:as _request ::keys [extra-csp-srcs]}]
(str/join
";"
;; Load anything from the clojars domain
["default-src 'self'"
;; Load images from clojars domain along with dnsimple's logo and any extra
;; allowed sources per page
(apply str "img-src 'self' https://cdn.dnsimple.com "
(interpose " " extra-csp-img-src))]))
(concat
;; Load anything from the clojars domain
["default-src 'self'"
;; Load images from clojars domain along with dnsimple's logo and any extra
;; allowed sources per page
(apply str "img-src 'self' https://cdn.dnsimple.com "
(interpose " " (:img-src extra-csp-srcs)))]
(for [[k v] (dissoc extra-csp-srcs :img-src)]
(apply str (name k) " 'self' " (interpose " " v))))))

(def ^:private permissions-policy
;; We only need to write to the clipboard
Expand All @@ -72,10 +75,10 @@
;; referrer with non-secure sites.
(assoc-in [:headers "Referrer-Policy"] "no-referrer-when-downgrade")))))

(defn with-extra-img-src
"Adds an additional img-src to our content-security-policy."
[src body]
(defn with-extra-csp-srcs
"Adds an additional *-src values to our content-security-policy."
[srcs body]
(-> body
(response)
(content-type "text/html;charset=utf-8")
(assoc ::extra-csp-img-src src)))
(assoc ::extra-csp-srcs srcs)))
2 changes: 1 addition & 1 deletion src/clojars/log.clj
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

java.util.List
(redact [v]
(vec (map redact v)))
(mapv redact v))

java.util.Map
(redact [v]
Expand Down
2 changes: 1 addition & 1 deletion src/clojars/routes/artifact.clj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
(defn- with-shields-io-img-src
"Allows shields.io badges to be shown on artifact pages."
[body]
(http-utils/with-extra-img-src ["https://img.shields.io"] body))
(http-utils/with-extra-csp-srcs {:img-src ["https://img.shields.io"]} body))

(defn show [db stats group-id artifact-id]
(when-some [artifact (db/find-jar db group-id artifact-id)]
Expand Down
8 changes: 5 additions & 3 deletions src/clojars/routes/user.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[clojars.auth :as auth]
[clojars.db :as db]
[clojars.event :as event]
[clojars.hcaptcha :as hcaptcha]
[clojars.http-utils :as http-utils]
[clojars.log :as log]
[clojars.routes.common :as common]
Expand All @@ -19,7 +20,7 @@
(defn- with-data-img-src
"Allows data: badges to be shown on the mfa page to allow the qrcode to load."
[body]
(http-utils/with-extra-img-src ["data:"] body))
(http-utils/with-extra-csp-srcs {:img-src ["data:"]} body))

(defn- create-mfa [db
account
Expand Down Expand Up @@ -115,7 +116,7 @@
(assoc (redirect "/mfa")
:flash "Password incorrect.")))))

(defn routes [db event-emitter mailer]
(defn routes [db event-emitter hcaptcha mailer]
(compojure/routes
(GET "/profile" {:keys [flash]}
(auth/with-account
Expand Down Expand Up @@ -146,7 +147,8 @@
#(view/update-notifications db % params)))

(GET "/register" {:keys [params flash]}
(view/register-form params flash))
(http-utils/with-extra-csp-srcs hcaptcha/hcaptcha-csp
(view/register-form hcaptcha params flash)))

(GET "/forgot-password" _
(view/forgot-password-form))
Expand Down
6 changes: 4 additions & 2 deletions src/clojars/system.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
(:require
[clojars.email :refer [simple-mailer]]
[clojars.event :as event]
[clojars.hcaptcha :as hcaptcha]
[clojars.notifications :as notifications]
;; for defmethods
[clojars.notifications.admin]
Expand Down Expand Up @@ -81,6 +82,7 @@
(:callback-uri gitlab-oauth))
:event-emitter (event/new-sqs-emitter (:event-queue config))
:event-receiver (event/new-sqs-receiver (:event-queue config))
:hcaptcha (hcaptcha/new-hcaptcha (:hcaptcha config))
:http (jetty-server (assoc (:http config)
:configurator patch/use-status-message-header))
:http-client (remote-service/new-http-remote-service)
Expand All @@ -91,8 +93,8 @@
:storage (storage-component (:repo config) (:cdn-token config) (:cdn-url config))))
(component/system-using
{:app [:clojars-app]
:clojars-app [:db :github :gitlab :error-reporter :event-emitter :http-client
:mailer :stats :search :storage]
:clojars-app [:db :github :gitlab :error-reporter :event-emitter :hcaptcha
:http-client :mailer :stats :search :storage]
:event-emitter [:error-reporter]
:http [:app]
:notifications [:db :mailer]
Expand Down
7 changes: 4 additions & 3 deletions src/clojars/web.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
:status 400})))))

(defn- main-routes
[{:as _system :keys [db event-emitter mailer search stats]}]
[{:as _system :keys [db event-emitter hcaptcha mailer search stats]}]
(let [db (:spec db)]
(routes
(GET "/" _
Expand Down Expand Up @@ -83,7 +83,7 @@
(artifact/routes db stats)
;; user routes must go after artifact routes
;; since they both catch /:identifier
(user/routes db event-emitter mailer)
(user/routes db event-emitter hcaptcha mailer)
(verify/routes db event-emitter)
(token/routes db)
(api/routes db stats)
Expand Down Expand Up @@ -139,6 +139,7 @@
:keys [db
error-reporter
event-emitter
hcaptcha
http-client
github
gitlab
Expand Down Expand Up @@ -168,7 +169,7 @@
(friend/authenticate
{:credential-fn (auth/password-credential-fn db event-emitter)
:workflows [(auth/interactive-form-with-mfa-workflow)
(registration/workflow db)
(registration/workflow db hcaptcha)
(github/workflow github http-client db)
(gitlab/workflow gitlab http-client db)]})
(wrap-reject-invalid-params)
Expand Down
28 changes: 23 additions & 5 deletions src/clojars/web/user.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
(:require
[buddy.core.codecs.base64 :as base64]
[cemerick.friend.credentials :as creds]
[clojars.config :refer [config]]
[clojars.config :as config]
[clojars.db :as db :refer [find-group-verification
find-groupnames
find-user
Expand All @@ -13,6 +13,7 @@
update-user
update-user-notifications]]
[clojars.event :as event]
[clojars.hcaptcha :as hcaptcha]
[clojars.log :as log]
[clojars.notifications.common :as notif-common]
[clojars.web.common :refer [html-doc error-list form-table jar-link
Expand All @@ -34,7 +35,8 @@

(set! *warn-on-reflection* true)

(defn register-form [{:keys [errors email username]} message]
(defn register-form
[hcaptcha {:keys [errors email username]} message]
(html-doc "Register" {}
[:div.small-section
[:h1 "Register"]
Expand All @@ -59,6 +61,17 @@
(password-field {:placeholder "confirm your password"
:required true}
:confirm)
;; We don't load the captcha js in tests, since we can't interact
;; with it. Instead, we emit a text area that we can fill with a
;; response value that is valid or not when using an hcaptcha
;; test token..
(if (config/test-profile?)
(list
(label :h-captcha-response "TEST Captcha")
(text-field :h-captcha-response))
(list
[:div.h-captcha {:data-sitekey (hcaptcha/site-key hcaptcha)}]
[:script {:src "https://js.hcaptcha.com/1/api.js" :async true :defer true}]))
(submit-button "Register"))]))

;; Validations
Expand Down Expand Up @@ -97,15 +110,20 @@
"letters, numbers, hyphens and underscores.")]
[:username pred/present? "Username can't be blank"]])))

(defn- captcha-validations
[hcaptcha]
[[:captcha (partial hcaptcha/valid-response? hcaptcha) "Captcha response is invalid."]])

(defn new-user-validations
[db confirm]
[db hcaptcha confirm]
(concat [[:password pred/present? "Password can't be blank"]
[:username #(not (or (reserved-names %)
(find-user db %)
(seq (group-activenames db %))))
"Username is already taken"]]
(user-validations db)
(password-validations confirm)))
(password-validations confirm)
(captcha-validations hcaptcha)))

(defn reset-password-validations [db confirm]
(concat
Expand Down Expand Up @@ -249,7 +267,7 @@
(log/with-context {:email-or-username email-or-username}
(if-let [user (find-user-by-user-or-email db email-or-username)]
(let [reset-code (db/set-password-reset-code! db (:user user))
base-url (:base-url (config))
base-url (:base-url (config/config))
reset-password-url (str base-url "/password-resets/" reset-code)]
(log/info {:tag :password-reset-code-generated})
(mailer (:email user)
Expand Down
8 changes: 8 additions & 0 deletions test/clojars/integration/steps.clj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
(cond-> otp (fill-in "Two-Factor Code" otp))
(press "Login"))))

(defn fill-in-captcha
([state]
;; From https://docs.hcaptcha.com/#integration-testing-test-keys
(fill-in-captcha state "10000000-aaaa-bbbb-cccc-000000000001"))
([state value]
(fill-in state "TEST Captcha" value)))

(defn register-as
([state user email password]
(register-as state user email password password))
Expand All @@ -35,6 +42,7 @@
(fill-in "Username" user)
(fill-in "Password" password)
(fill-in "Confirm password" confirm)
(fill-in-captcha)
(press "Register"))))

(defn create-deploy-token
Expand Down
Loading