93 lines
2.9 KiB
Go
93 lines
2.9 KiB
Go
|
|
package rest
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
|
||
|
|
"github.com/explorer/backend/auth"
|
||
|
|
)
|
||
|
|
|
||
|
|
// handleAuthRefresh implements POST /api/v1/auth/refresh.
|
||
|
|
//
|
||
|
|
// Contract:
|
||
|
|
// - Requires a valid, unrevoked wallet JWT in the Authorization header.
|
||
|
|
// - Mints a new JWT for the same address+track with a fresh jti and a
|
||
|
|
// fresh per-track TTL.
|
||
|
|
// - Revokes the presented token so it cannot be reused.
|
||
|
|
//
|
||
|
|
// This is the mechanism that makes the short Track-4 TTL (60 min in
|
||
|
|
// PR #8) acceptable: operators refresh while the token is still live
|
||
|
|
// rather than re-signing a SIWE message every hour.
|
||
|
|
func (s *Server) handleAuthRefresh(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != http.MethodPost {
|
||
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if s.walletAuth == nil {
|
||
|
|
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "wallet auth not configured")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
token := extractBearerToken(r)
|
||
|
|
if token == "" {
|
||
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "missing or malformed Authorization header")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, err := s.walletAuth.RefreshJWT(r.Context(), token)
|
||
|
|
if err != nil {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, auth.ErrJWTRevoked):
|
||
|
|
writeError(w, http.StatusUnauthorized, "token_revoked", err.Error())
|
||
|
|
case errors.Is(err, auth.ErrWalletAuthStorageNotInitialized):
|
||
|
|
writeError(w, http.StatusServiceUnavailable, "service_unavailable", err.Error())
|
||
|
|
default:
|
||
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", err.Error())
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
_ = json.NewEncoder(w).Encode(resp)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleAuthLogout implements POST /api/v1/auth/logout.
|
||
|
|
//
|
||
|
|
// Records the presented token's jti in jwt_revocations so subsequent
|
||
|
|
// calls to ValidateJWT will reject it. Idempotent: logging out twice
|
||
|
|
// with the same token succeeds.
|
||
|
|
func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if r.Method != http.MethodPost {
|
||
|
|
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if s.walletAuth == nil {
|
||
|
|
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "wallet auth not configured")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
token := extractBearerToken(r)
|
||
|
|
if token == "" {
|
||
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "missing or malformed Authorization header")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := s.walletAuth.RevokeJWT(r.Context(), token, "logout"); err != nil {
|
||
|
|
switch {
|
||
|
|
case errors.Is(err, auth.ErrJWTRevocationStorageMissing):
|
||
|
|
// Surface 503 so ops know migration 0016 hasn't run; the
|
||
|
|
// client should treat the token as logged out locally.
|
||
|
|
writeError(w, http.StatusServiceUnavailable, "service_unavailable", err.Error())
|
||
|
|
default:
|
||
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", err.Error())
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||
|
|
"status": "ok",
|
||
|
|
})
|
||
|
|
}
|