Skip to content

Commit

Permalink
Add hcaptcha to signup form
Browse files Browse the repository at this point in the history
This should help prevent automated spam signups.

Implements #628.
  • Loading branch information
tobias committed Sep 7, 2024
1 parent 2b9d410 commit 51eec12
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 29 deletions.
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)}))
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
6 changes: 4 additions & 2 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 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

0 comments on commit 51eec12

Please sign in to comment.