Add Altcha captcha provider support
Some checks are pending
continuous-integration/drone/push Build is running

This commit is contained in:
nemunaire 2026-02-13 12:28:12 +07:00
commit e0d8526577
8 changed files with 160 additions and 7 deletions

View file

@ -38,6 +38,17 @@ func DeclareAuthenticationRoutes(cfg *happydns.Options, baserouter, apirouter *g
apirouter.POST("/auth", lc.Login)
apirouter.POST("/auth/logout", lc.Logout)
if localChallenge, ok := dependancies.CaptchaVerifier().(happydns.CaptchaLocalChallenge); ok {
apirouter.GET("/auth/challenge", func(c *gin.Context) {
challenge, err := localChallenge.NewChallenge()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Data(http.StatusOK, "application/json", challenge)
})
}
if len(cfg.OIDCClients) > 0 {
oidcp := controller.NewOIDCProvider(cfg, dependancies.AuthenticationUsecase())

View file

@ -0,0 +1,94 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package captcha
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"log"
altcha "github.com/altcha-org/altcha-lib-go"
)
var (
altchaComplexity int64
altchaHMACKey string
)
func init() {
flag.Int64Var(&altchaComplexity, "altcha-complexity", 100_000, "Serves as a measure to balance security against automated abuse/spam and user experience")
flag.StringVar(&altchaHMACKey, "altcha-hmac-key", "", "Secret HMAC key for Altcha challenge signing and verification")
}
type AltchaVerifier struct {
options altcha.ChallengeOptions
}
func NewAltchaVerifier() *AltchaVerifier {
if altchaHMACKey == "" {
b := make([]byte, 24)
_, err := rand.Read(b)
if err != nil {
log.Fatalf("error generating Altcha HMAC key: %v", err)
}
altchaHMACKey = base64.URLEncoding.EncodeToString(b)[:32]
}
return &AltchaVerifier{
options: altcha.ChallengeOptions{
HMACKey: altchaHMACKey,
MaxNumber: altchaComplexity,
},
}
}
func (a *AltchaVerifier) Provider() string { return "altcha" }
func (a *AltchaVerifier) SiteKey() string { return "" }
func (a *AltchaVerifier) Verify(token, _ string) error {
ok, err := altcha.VerifySolution(token, altchaHMACKey, true)
if err != nil {
return fmt.Errorf("altcha verification failed: %w", err)
}
if !ok {
return fmt.Errorf("altcha verification failed: invalid solution")
}
return nil
}
// NewAltchaChallenge generates a new Altcha challenge to be served to the frontend.
func (a *AltchaVerifier) NewChallenge() (json.RawMessage, error) {
challenge, err := altcha.CreateChallenge(a.options)
if err != nil {
return nil, fmt.Errorf("failed to create altcha challenge: %w", err)
}
data, err := json.Marshal(challenge)
if err != nil {
return nil, fmt.Errorf("failed to marshal altcha challenge: %w", err)
}
return data, nil
}

View file

@ -30,6 +30,8 @@ import (
// Returns a no-op verifier when provider is empty.
func NewVerifier(provider string) happydns.CaptchaVerifier {
switch provider {
case "altcha":
return NewAltchaVerifier()
case "hcaptcha":
return &hCaptchaVerifier{}
case "recaptchav2":

View file

@ -57,7 +57,7 @@ func declareFlags(o *happydns.Options) {
flag.StringVar(&o.MailSMTPPassword, "mail-smtp-password", o.MailSMTPPassword, "Password associated with the given username for SMTP authentication")
flag.BoolVar(&o.MailSMTPTLSSNoVerify, "mail-smtp-tls-no-verify", o.MailSMTPTLSSNoVerify, "Do not verify certificate validity on SMTP connection")
flag.StringVar(&o.CaptchaProvider, "captcha-provider", o.CaptchaProvider, "Captcha provider to use for bot protection (hcaptcha, recaptchav2, turnstile, or empty to disable)")
flag.StringVar(&o.CaptchaProvider, "captcha-provider", o.CaptchaProvider, "Captcha provider to use for bot protection (altcha, hcaptcha, recaptchav2, turnstile, or empty to disable)")
flag.IntVar(&o.CaptchaLoginThreshold, "captcha-login-threshold", 3, "Number of failed login attempts before captcha is required (0 = always require when provider configured)")
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations