web/backend/secretapi/session_auth.go

98 lines
3.0 KiB
Go

package main
import (
"context"
"net/http"
"strings"
"time"
)
const (
sessionHeaderToken = "X-Edut-Session"
sessionHeaderExpiresAt = "X-Edut-Session-Expires-At"
)
func (a *app) issueWalletSession(ctx context.Context, wallet, designationCode string) (walletSessionRecord, error) {
token, err := randomHex(24)
if err != nil {
return walletSessionRecord{}, err
}
now := time.Now().UTC()
rec := walletSessionRecord{
SessionToken: token,
Wallet: strings.ToLower(strings.TrimSpace(wallet)),
DesignationCode: strings.TrimSpace(designationCode),
ChainID: a.cfg.ChainID,
IssuedAt: now,
ExpiresAt: now.Add(a.cfg.WalletSessionTTL),
LastSeenAt: &now,
}
if err := a.store.putWalletSession(ctx, rec); err != nil {
return walletSessionRecord{}, err
}
return rec, nil
}
func sessionTokenFromRequest(r *http.Request) string {
auth := strings.TrimSpace(r.Header.Get("Authorization"))
if auth != "" {
lower := strings.ToLower(auth)
if strings.HasPrefix(lower, "bearer ") {
token := strings.TrimSpace(auth[len("Bearer "):])
if token != "" {
return token
}
}
}
if token := strings.TrimSpace(r.Header.Get(sessionHeaderToken)); token != "" {
return token
}
return strings.TrimSpace(r.URL.Query().Get("session_token"))
}
func (a *app) enforceWalletSession(w http.ResponseWriter, r *http.Request, wallet string) bool {
sessionToken := sessionTokenFromRequest(r)
if sessionToken == "" {
if a.cfg.RequireWalletSession {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_required", "wallet session required")
return false
}
return true
}
rec, err := a.store.getWalletSession(r.Context(), sessionToken)
if err != nil {
if err == errNotFound {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_invalid", "wallet session not found")
return false
}
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to resolve wallet session")
return false
}
if rec.RevokedAt != nil {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_revoked", "wallet session revoked")
return false
}
if rec.ChainID != a.cfg.ChainID {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_invalid", "wallet session chain mismatch")
return false
}
now := time.Now().UTC()
if !now.Before(rec.ExpiresAt.UTC()) {
writeErrorCode(w, http.StatusUnauthorized, "wallet_session_expired", "wallet session expired")
return false
}
if !strings.EqualFold(strings.TrimSpace(rec.Wallet), strings.TrimSpace(wallet)) {
writeErrorCode(w, http.StatusForbidden, "wallet_session_mismatch", "wallet session does not match requested wallet")
return false
}
if err := a.store.touchWalletSession(r.Context(), rec.SessionToken, now); err != nil && err != errNotFound {
writeErrorCode(w, http.StatusInternalServerError, "store_error", "failed to update wallet session")
return false
}
w.Header().Set(sessionHeaderToken, rec.SessionToken)
w.Header().Set(sessionHeaderExpiresAt, rec.ExpiresAt.UTC().Format(time.RFC3339Nano))
return true
}