CAPTCHAと2FAを実装
This commit is contained in:
@@ -104,3 +104,23 @@ func (s *AuthService) UpdateProfile(userID uint, name string) error {
|
||||
user.Name = name
|
||||
return s.userRepo.Update(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) EnableTOTP(userID uint, secret string) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
user.TOTPSecret = secret
|
||||
user.TOTPEnabled = true
|
||||
return s.userRepo.Update(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) DisableTOTP(userID uint) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
user.TOTPSecret = ""
|
||||
user.TOTPEnabled = false
|
||||
return s.userRepo.Update(user)
|
||||
}
|
||||
|
||||
75
internal/service/captcha_service.go
Normal file
75
internal/service/captcha_service.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/captcha"
|
||||
)
|
||||
|
||||
type CaptchaService struct {
|
||||
captchaType string
|
||||
turnstileSecretKey string
|
||||
}
|
||||
|
||||
func NewCaptchaService(captchaType, turnstileSecretKey string) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
captchaType: captchaType,
|
||||
turnstileSecretKey: turnstileSecretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CaptchaService) NewImageCaptcha() string {
|
||||
return captcha.New()
|
||||
}
|
||||
func (s *CaptchaService) VerifyImageCaptcha(id, answer string) bool {
|
||||
return captcha.VerifyString(id, answer)
|
||||
}
|
||||
|
||||
type turnstileResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []string `json:"error-codes"`
|
||||
}
|
||||
|
||||
func (s *CaptchaService) VerifyTurnstile(token, remoteIP string) (bool, error) {
|
||||
if token == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("secret", s.turnstileSecretKey)
|
||||
form.Set("response", token)
|
||||
if remoteIP != "" {
|
||||
form.Set("remoteip", remoteIP)
|
||||
}
|
||||
|
||||
resp, err := http.Post(
|
||||
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
|
||||
"application/x-www-form-urlencoded",
|
||||
strings.NewReader(form.Encode()),
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("turnstile request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("reading turnstile response: %w", err)
|
||||
}
|
||||
|
||||
var result turnstileResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return false, fmt.Errorf("parsing turnstile response: %w", err)
|
||||
}
|
||||
|
||||
return result.Success, nil
|
||||
}
|
||||
|
||||
func (s *CaptchaService) Type() string {
|
||||
return s.captchaType
|
||||
}
|
||||
71
internal/service/totp_service.go
Normal file
71
internal/service/totp_service.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"net/url"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
totplib "github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
type TOTPService struct{}
|
||||
|
||||
func NewTOTPService() *TOTPService {
|
||||
return &TOTPService{}
|
||||
}
|
||||
|
||||
type TOTPSetupData struct {
|
||||
Secret string
|
||||
QRCodeB64 string
|
||||
OTPAuthURL string
|
||||
}
|
||||
|
||||
func (s *TOTPService) GenerateSecret(email, issuer string) (*TOTPSetupData, error) {
|
||||
key, err := totplib.Generate(totplib.GenerateOpts{
|
||||
Issuer: issuer,
|
||||
AccountName: email,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildSetupData(key)
|
||||
}
|
||||
|
||||
func (s *TOTPService) SetupDataFromSecret(secret, email, issuer string) (*TOTPSetupData, error) {
|
||||
otpURL := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s",
|
||||
url.PathEscape(issuer),
|
||||
url.PathEscape(email),
|
||||
url.QueryEscape(secret),
|
||||
url.QueryEscape(issuer),
|
||||
)
|
||||
key, err := otp.NewKeyFromURL(otpURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildSetupData(key)
|
||||
}
|
||||
|
||||
func (s *TOTPService) buildSetupData(key *otp.Key) (*TOTPSetupData, error) {
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TOTPSetupData{
|
||||
Secret: key.Secret(),
|
||||
QRCodeB64: base64.StdEncoding.EncodeToString(buf.Bytes()),
|
||||
OTPAuthURL: key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *TOTPService) Validate(secret, code string) bool {
|
||||
return totplib.Validate(code, secret)
|
||||
}
|
||||
Reference in New Issue
Block a user