mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-22 00:59:11 +01:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
147 lines
3.7 KiB
Go
147 lines
3.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/bytedance/sonic"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/yusing/godoxy/internal/common"
|
|
httputils "github.com/yusing/goutils/http"
|
|
strutils "github.com/yusing/goutils/strings"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
var ErrInvalidUsername = errors.New("invalid username")
|
|
|
|
type (
|
|
UserPassAuth struct {
|
|
username string
|
|
pwdHash []byte
|
|
secret []byte
|
|
tokenTTL time.Duration
|
|
}
|
|
UserPassClaims struct {
|
|
Username string `json:"username"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
)
|
|
|
|
var _ Provider = (*UserPassAuth)(nil)
|
|
|
|
func NewUserPassAuth(username, password string, secret []byte, tokenTTL time.Duration) (*UserPassAuth, error) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &UserPassAuth{
|
|
username: username,
|
|
pwdHash: hash,
|
|
secret: secret,
|
|
tokenTTL: tokenTTL,
|
|
}, nil
|
|
}
|
|
|
|
func NewUserPassAuthFromEnv() (*UserPassAuth, error) {
|
|
return NewUserPassAuth(
|
|
common.APIUser,
|
|
common.APIPassword,
|
|
common.APIJWTSecret,
|
|
common.APIJWTTokenTTL,
|
|
)
|
|
}
|
|
|
|
func (auth *UserPassAuth) TokenCookieName() string {
|
|
return "godoxy_token"
|
|
}
|
|
|
|
func (auth *UserPassAuth) NewToken() (token string, err error) {
|
|
claim := &UserPassClaims{
|
|
Username: auth.username,
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(auth.tokenTTL)),
|
|
},
|
|
}
|
|
tok := jwt.NewWithClaims(jwt.SigningMethodHS512, claim)
|
|
token, err = tok.SignedString(auth.secret)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func (auth *UserPassAuth) CheckToken(r *http.Request) error {
|
|
jwtCookie, err := r.Cookie(auth.TokenCookieName())
|
|
if err != nil {
|
|
return ErrMissingSessionToken
|
|
}
|
|
var claims UserPassClaims
|
|
token, err := jwt.ParseWithClaims(jwtCookie.Value, &claims, func(t *jwt.Token) (interface{}, error) {
|
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
|
}
|
|
return auth.secret, nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case !token.Valid:
|
|
return ErrInvalidSessionToken
|
|
case claims.Username != auth.username:
|
|
return fmt.Errorf("%w: %s", ErrUserNotAllowed, claims.Username)
|
|
case claims.ExpiresAt.Before(time.Now()):
|
|
return fmt.Errorf("token expired on %s", strutils.FormatTime(claims.ExpiresAt.Time))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type UserPassAuthCallbackRequest struct {
|
|
User string `json:"username"`
|
|
Pass string `json:"password"`
|
|
}
|
|
|
|
func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
var creds UserPassAuthCallbackRequest
|
|
err := sonic.ConfigDefault.NewDecoder(r.Body).Decode(&creds)
|
|
if err != nil {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := auth.validatePassword(creds.User, creds.Pass); err != nil {
|
|
// NOTE: do not include the actual error here
|
|
http.Error(w, "invalid credentials", http.StatusBadRequest)
|
|
return
|
|
}
|
|
token, err := auth.NewToken()
|
|
if err != nil {
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
httputils.LogError(r).Msg(fmt.Sprintf("failed to generate token: %v", err))
|
|
return
|
|
}
|
|
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/login", http.StatusFound)
|
|
}
|
|
|
|
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|
ClearTokenCookie(w, r, auth.TokenCookieName())
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
}
|
|
|
|
func (auth *UserPassAuth) validatePassword(user, pass string) error {
|
|
if user != auth.username {
|
|
return ErrInvalidUsername
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword(auth.pwdHash, []byte(pass)); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|