feat: add captcha when signing up (closes #53)
This commit is contained in:
parent
bb3d56f548
commit
6f7eb5eeee
23 changed files with 316 additions and 61 deletions
58
backend/routes/auth/captcha.go
Normal file
58
backend/routes/auth/captcha.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
)
|
||||
|
||||
const hcaptchaURL = "https://hcaptcha.com/siteverify"
|
||||
|
||||
type hcaptchaResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// verifyCaptcha verifies a captcha response.
|
||||
func (s *Server) verifyCaptcha(ctx context.Context, response string) (ok bool, err error) {
|
||||
vals := url.Values{
|
||||
"response": []string{response},
|
||||
"secret": []string{s.hcaptchaSecret},
|
||||
"sitekey": []string{s.hcaptchaSitekey},
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", hcaptchaURL, strings.NewReader(vals.Encode()))
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "creating request")
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", "pronouns.cc/"+server.Tag)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "sending request")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
return false, errors.Sentinel("error status code")
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "reading body")
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
var hr hcaptchaResponse
|
||||
err = json.Unmarshal(b, &hr)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unmarshaling json")
|
||||
}
|
||||
|
||||
return hr.Success, nil
|
||||
}
|
|
@ -39,9 +39,10 @@ type discordCallbackResponse struct {
|
|||
Token string `json:"token,omitempty"`
|
||||
User *userResponse `json:"user,omitempty"`
|
||||
|
||||
Discord string `json:"discord,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
Discord string `json:"discord,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
RequireCaptcha bool `json:"require_captcha"`
|
||||
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
|
@ -148,10 +149,11 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
render.JSON(w, r, discordCallbackResponse{
|
||||
HasAccount: false,
|
||||
Discord: du.String(),
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
HasAccount: false,
|
||||
Discord: du.String(),
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
RequireCaptcha: s.hcaptchaSecret != "",
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -251,9 +253,10 @@ func (s *Server) discordUnlink(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
type signupRequest struct {
|
||||
Ticket string `json:"ticket"`
|
||||
Username string `json:"username"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
Ticket string `json:"ticket"`
|
||||
Username string `json:"username"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
CaptchaResponse string `json:"captcha_response"`
|
||||
}
|
||||
|
||||
type signupResponse struct {
|
||||
|
@ -298,6 +301,19 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
return server.APIError{Code: server.ErrInvalidTicket}
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if s.hcaptchaSecret != "" {
|
||||
ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse)
|
||||
if err != nil {
|
||||
log.Errorf("verifying captcha: %v", err)
|
||||
return server.APIError{Code: server.ErrInternalServerError}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return server.APIError{Code: server.ErrInvalidCaptcha}
|
||||
}
|
||||
}
|
||||
|
||||
u, err := s.DB.CreateUser(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
||||
|
|
|
@ -27,9 +27,10 @@ type fediCallbackResponse struct {
|
|||
Token string `json:"token,omitempty"`
|
||||
User *userResponse `json:"user,omitempty"`
|
||||
|
||||
Fediverse string `json:"fediverse,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
Fediverse string `json:"fediverse,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
RequireCaptcha bool `json:"require_captcha"`
|
||||
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
|
@ -169,10 +170,11 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error
|
|||
}
|
||||
|
||||
render.JSON(w, r, fediCallbackResponse{
|
||||
HasAccount: false,
|
||||
Fediverse: mu.Username,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
HasAccount: false,
|
||||
Fediverse: mu.Username,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
RequireCaptcha: s.hcaptchaSecret != "",
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -278,10 +280,11 @@ func (s *Server) mastodonUnlink(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
type fediSignupRequest struct {
|
||||
Instance string `json:"instance"`
|
||||
Ticket string `json:"ticket"`
|
||||
Username string `json:"username"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
Instance string `json:"instance"`
|
||||
Ticket string `json:"ticket"`
|
||||
Username string `json:"username"`
|
||||
InviteCode string `json:"invite_code"`
|
||||
CaptchaResponse string `json:"captcha_response"`
|
||||
}
|
||||
|
||||
func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
||||
|
@ -326,6 +329,19 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
return server.APIError{Code: server.ErrInvalidTicket}
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if s.hcaptchaSecret != "" {
|
||||
ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse)
|
||||
if err != nil {
|
||||
log.Errorf("verifying captcha: %v", err)
|
||||
return server.APIError{Code: server.ErrInternalServerError}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return server.APIError{Code: server.ErrInvalidCaptcha}
|
||||
}
|
||||
}
|
||||
|
||||
u, err := s.DB.CreateUser(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
||||
|
|
|
@ -149,10 +149,11 @@ func (s *Server) misskeyCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
render.JSON(w, r, fediCallbackResponse{
|
||||
HasAccount: false,
|
||||
Fediverse: mu.User.Username,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
HasAccount: false,
|
||||
Fediverse: mu.User.Username,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
RequireCaptcha: s.hcaptchaSecret != "",
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -256,6 +257,19 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
|
|||
return server.APIError{Code: server.ErrInvalidTicket}
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if s.hcaptchaSecret != "" {
|
||||
ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse)
|
||||
if err != nil {
|
||||
log.Errorf("verifying captcha: %v", err)
|
||||
return server.APIError{Code: server.ErrInternalServerError}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return server.APIError{Code: server.ErrInvalidCaptcha}
|
||||
}
|
||||
}
|
||||
|
||||
u, err := s.DB.CreateUser(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
||||
|
|
|
@ -33,9 +33,10 @@ type googleCallbackResponse struct {
|
|||
Token string `json:"token,omitempty"`
|
||||
User *userResponse `json:"user,omitempty"`
|
||||
|
||||
Google string `json:"google,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
Google string `json:"google,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
RequireCaptcha bool `json:"require_captcha"`
|
||||
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
|
@ -167,10 +168,11 @@ func (s *Server) googleCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
render.JSON(w, r, googleCallbackResponse{
|
||||
HasAccount: false,
|
||||
Google: googleUsername,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
HasAccount: false,
|
||||
Google: googleUsername,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
RequireCaptcha: s.hcaptchaSecret != "",
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -302,6 +304,19 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
return server.APIError{Code: server.ErrInvalidTicket}
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if s.hcaptchaSecret != "" {
|
||||
ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse)
|
||||
if err != nil {
|
||||
log.Errorf("verifying captcha: %v", err)
|
||||
return server.APIError{Code: server.ErrInternalServerError}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return server.APIError{Code: server.ErrInvalidCaptcha}
|
||||
}
|
||||
}
|
||||
|
||||
u, err := s.DB.CreateUser(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
||||
|
|
|
@ -18,6 +18,9 @@ type Server struct {
|
|||
|
||||
RequireInvite bool
|
||||
BaseURL string
|
||||
|
||||
hcaptchaSitekey string
|
||||
hcaptchaSecret string
|
||||
}
|
||||
|
||||
type userResponse struct {
|
||||
|
@ -70,9 +73,11 @@ func dbUserToUserResponse(u db.User, fields []db.Field) *userResponse {
|
|||
|
||||
func Mount(srv *server.Server, r chi.Router) {
|
||||
s := &Server{
|
||||
Server: srv,
|
||||
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
||||
BaseURL: os.Getenv("BASE_URL"),
|
||||
Server: srv,
|
||||
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
||||
BaseURL: os.Getenv("BASE_URL"),
|
||||
hcaptchaSitekey: os.Getenv("HCAPTCHA_SITEKEY"),
|
||||
hcaptchaSecret: os.Getenv("HCAPTCHA_SECRET"),
|
||||
}
|
||||
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
|
|
|
@ -55,9 +55,10 @@ type tumblrCallbackResponse struct {
|
|||
Token string `json:"token,omitempty"`
|
||||
User *userResponse `json:"user,omitempty"`
|
||||
|
||||
Tumblr string `json:"tumblr,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
Tumblr string `json:"tumblr,omitempty"` // username, for UI purposes
|
||||
Ticket string `json:"ticket,omitempty"`
|
||||
RequireInvite bool `json:"require_invite"` // require an invite for signing up
|
||||
RequireCaptcha bool `json:"require_captcha"`
|
||||
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
|
@ -200,10 +201,11 @@ func (s *Server) tumblrCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
render.JSON(w, r, tumblrCallbackResponse{
|
||||
HasAccount: false,
|
||||
Tumblr: tumblrName,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
HasAccount: false,
|
||||
Tumblr: tumblrName,
|
||||
Ticket: ticket,
|
||||
RequireInvite: s.RequireInvite,
|
||||
RequireCaptcha: s.hcaptchaSecret != "",
|
||||
})
|
||||
|
||||
return nil
|
||||
|
@ -335,6 +337,19 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
return server.APIError{Code: server.ErrInvalidTicket}
|
||||
}
|
||||
|
||||
// check captcha
|
||||
if s.hcaptchaSecret != "" {
|
||||
ok, err := s.verifyCaptcha(ctx, req.CaptchaResponse)
|
||||
if err != nil {
|
||||
log.Errorf("verifying captcha: %v", err)
|
||||
return server.APIError{Code: server.ErrInternalServerError}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return server.APIError{Code: server.ErrInvalidCaptcha}
|
||||
}
|
||||
}
|
||||
|
||||
u, err := s.DB.CreateUser(ctx, tx, req.Username)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == db.ErrUsernameTaken {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue