Files
boc/aamos-admin-upgrade/backend/middleware/auth.go
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

140 lines
3.3 KiB
Go

package middleware
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"aamos-admin/config"
"aamos-admin/models"
)
type contextKey string
const userKey contextKey = "user"
type jwtHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
type jwtClaims struct {
Sub string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Role models.Role `json:"role"`
Exp int64 `json:"exp"`
Iat int64 `json:"iat"`
}
var (
errMalformed = errors.New("malformed token")
errSignature = errors.New("invalid signature")
errExpired = errors.New("token expired")
errAlgorithm = errors.New("unexpected algorithm")
)
// Auth returns a middleware that validates a Bearer JWT (HMAC-SHA256).
// On success the authenticated *models.User is placed in the request context.
// On failure a 401 JSON response is written and the chain is halted.
func Auth(cfg *config.Config) func(http.Handler) http.Handler {
secret := []byte(cfg.JWTSecret)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
raw, ok := bearerToken(r)
if !ok {
writeUnauthorized(w, "missing bearer token")
return
}
claims, err := parseJWT(raw, secret)
if err != nil {
msg := "invalid token"
if errors.Is(err, errExpired) {
msg = "token expired"
}
writeUnauthorized(w, msg)
return
}
user := &models.User{
ID: claims.Sub,
Email: claims.Email,
Name: claims.Name,
Role: claims.Role,
}
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userKey, user)))
})
}
}
// UserFromContext returns the authenticated user stored in ctx, or nil.
func UserFromContext(ctx context.Context) *models.User {
u, _ := ctx.Value(userKey).(*models.User)
return u
}
func bearerToken(r *http.Request) (string, bool) {
h := r.Header.Get("Authorization")
t, found := strings.CutPrefix(h, "Bearer ")
if !found || t == "" {
return "", false
}
return t, true
}
func parseJWT(token string, secret []byte) (*jwtClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, errMalformed
}
headerJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return nil, errMalformed
}
var hdr jwtHeader
if err := json.Unmarshal(headerJSON, &hdr); err != nil {
return nil, errMalformed
}
if hdr.Alg != "HS256" {
return nil, errAlgorithm
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(parts[0] + "." + parts[1]))
expected := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(parts[2])) {
return nil, errSignature
}
payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, errMalformed
}
var claims jwtClaims
if err := json.Unmarshal(payloadJSON, &claims); err != nil {
return nil, errMalformed
}
if claims.Exp > 0 && time.Now().Unix() > claims.Exp {
return nil, errExpired
}
return &claims, nil
}
func writeUnauthorized(w http.ResponseWriter, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(struct {
Error string `json:"error"`
}{Error: msg})
}