feat: auth system hardening with token revocation, password management, and error sanitization
- Add token revocation with in-memory blacklist and periodic cleanup (SEC-M1)
- Add POST /api/auth/logout endpoint
- Fix OIDC auth_token cookie to HttpOnly with exchange endpoint (SEC-H3)
- Add password complexity validation (min 8 chars) (SEC-M2)
- Prevent admin self-deletion (SEC-M3)
- Add PUT /api/auth/users/{uid} for role/email updates (FUNC-M1)
- Add PUT /api/auth/users/{uid}/password for password changes (FUNC-H1)
- Sanitize error messages in auth handlers (SEC-M4)
This commit is contained in:
+181
-16
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -63,7 +64,8 @@ func (s *Server) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to generate token: "+err.Error())
|
slog.Error("failed to generate token", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,8 @@ func (s *Server) currentUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
user, err := s.store.GetUserByID(claims.UserID)
|
user, err := s.store.GetUserByID(claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to get user: "+err.Error())
|
slog.Error("failed to get user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +178,13 @@ func (s *Server) oidcCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
Role: "viewer", // OIDC users default to viewer; admin promotes via settings
|
Role: "viewer", // OIDC users default to viewer; admin promotes via settings
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to create user: "+err.Error())
|
slog.Error("failed to create user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to get user: "+err.Error())
|
slog.Error("failed to get user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,28 +195,53 @@ func (s *Server) oidcCallback(w http.ResponseWriter, r *http.Request) {
|
|||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to generate token: "+err.Error())
|
slog.Error("failed to generate token", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the token in a short-lived cookie the frontend can read once.
|
// Pass token via short-lived httpOnly cookie. The frontend reads it via
|
||||||
|
// a dedicated /api/auth/oidc/token endpoint and then the cookie is cleared.
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "auth_token",
|
Name: "auth_token",
|
||||||
Value: token.Token,
|
Value: token.Token,
|
||||||
Path: "/",
|
Path: "/api/auth/oidc",
|
||||||
MaxAge: 60, // 1 minute — frontend reads it immediately
|
MaxAge: 60,
|
||||||
HttpOnly: false,
|
HttpOnly: true,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
http.Redirect(w, r, "/?oidc=success", http.StatusFound)
|
http.Redirect(w, r, "/?oidc=success", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// oidcExchangeToken handles POST /api/auth/oidc/token — exchanges the httpOnly cookie for a JSON token.
|
||||||
|
func (s *Server) oidcExchangeToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cookie, err := r.Cookie("auth_token")
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
respondError(w, http.StatusUnauthorized, "no OIDC token available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cookie immediately.
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "auth_token",
|
||||||
|
Value: "",
|
||||||
|
Path: "/api/auth/oidc",
|
||||||
|
MaxAge: -1,
|
||||||
|
HttpOnly: true,
|
||||||
|
Secure: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{"token": cookie.Value})
|
||||||
|
}
|
||||||
|
|
||||||
// getAuthSettings handles GET /api/auth/settings.
|
// getAuthSettings handles GET /api/auth/settings.
|
||||||
func (s *Server) getAuthSettings(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) getAuthSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
as, err := s.store.GetAuthSettings()
|
as, err := s.store.GetAuthSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to get auth settings: "+err.Error())
|
slog.Error("failed to get auth settings", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Mask the client secret for the response.
|
// Mask the client secret for the response.
|
||||||
@@ -253,7 +283,8 @@ func (s *Server) updateAuthSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.store.UpdateAuthSettings(req); err != nil {
|
if err := s.store.UpdateAuthSettings(req); err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to update auth settings: "+err.Error())
|
slog.Error("failed to update auth settings", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +300,8 @@ func (s *Server) updateAuthSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
users, err := s.store.GetAllUsers()
|
users, err := s.store.GetAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to list users: "+err.Error())
|
slog.Error("failed to list users", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respondJSON(w, http.StatusOK, users)
|
respondJSON(w, http.StatusOK, users)
|
||||||
@@ -291,6 +323,10 @@ func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
respondError(w, http.StatusBadRequest, "username and password are required")
|
respondError(w, http.StatusBadRequest, "username and password are required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := validatePassword(req.Password); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if req.Role == "" {
|
if req.Role == "" {
|
||||||
req.Role = "viewer"
|
req.Role = "viewer"
|
||||||
@@ -302,7 +338,8 @@ func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
hash, err := auth.HashPassword(req.Password)
|
hash, err := auth.HashPassword(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to hash password: "+err.Error())
|
slog.Error("failed to hash password", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +350,8 @@ func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
Role: req.Role,
|
Role: req.Role,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to create user: "+err.Error())
|
slog.Error("failed to create user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +362,13 @@ func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
id := chi.URLParam(r, "uid")
|
id := chi.URLParam(r, "uid")
|
||||||
|
|
||||||
|
// Prevent deleting your own account.
|
||||||
|
claims, ok := auth.ClaimsFromContext(r.Context())
|
||||||
|
if ok && claims.UserID == id {
|
||||||
|
respondError(w, http.StatusBadRequest, "cannot delete your own account")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent deleting the last admin.
|
// Prevent deleting the last admin.
|
||||||
user, err := s.store.GetUserByID(id)
|
user, err := s.store.GetUserByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -331,7 +376,8 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
respondNotFound(w, "user")
|
respondNotFound(w, "user")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
respondError(w, http.StatusInternalServerError, "failed to get user: "+err.Error())
|
slog.Error("failed to get user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,9 +398,128 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.store.DeleteUser(id); err != nil {
|
if err := s.store.DeleteUser(id); err != nil {
|
||||||
respondError(w, http.StatusInternalServerError, "failed to delete user: "+err.Error())
|
slog.Error("failed to delete user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]string{"deleted": id})
|
respondJSON(w, http.StatusOK, map[string]string{"deleted": id})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validatePassword checks that a password meets minimum complexity requirements.
|
||||||
|
func validatePassword(password string) error {
|
||||||
|
if len(password) < 8 {
|
||||||
|
return fmt.Errorf("password must be at least 8 characters long")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// logout handles POST /api/auth/logout — revokes the current token.
|
||||||
|
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tokenStr := auth.ExtractToken(r)
|
||||||
|
if tokenStr != "" {
|
||||||
|
s.localAuth.RevokeToken(tokenStr)
|
||||||
|
}
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// changePassword handles PUT /api/auth/users/{uid}/password.
|
||||||
|
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid := chi.URLParam(r, "uid")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if !decodeJSON(w, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Password == "" {
|
||||||
|
respondError(w, http.StatusBadRequest, "password is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validatePassword(req.Password); err != nil {
|
||||||
|
respondError(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := auth.HashPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to hash password", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.UpdateUserPassword(uid, hash); err != nil {
|
||||||
|
if errors.Is(err, store.ErrNotFound) {
|
||||||
|
respondNotFound(w, "user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Error("failed to update password", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, map[string]string{"status": "password updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateUser handles PUT /api/auth/users/{uid}.
|
||||||
|
func (s *Server) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid := chi.URLParam(r, "uid")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
if !decodeJSON(w, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Role != "" && req.Role != "admin" && req.Role != "viewer" {
|
||||||
|
respondError(w, http.StatusBadRequest, "role must be 'admin' or 'viewer'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := s.store.GetUserByID(uid)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, store.ErrNotFound) {
|
||||||
|
respondNotFound(w, "user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Error("failed to get user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If demoting from admin, check we're not removing the last admin.
|
||||||
|
if existing.Role == "admin" && req.Role == "viewer" {
|
||||||
|
users, err := s.store.GetAllUsers()
|
||||||
|
if err == nil {
|
||||||
|
adminCount := 0
|
||||||
|
for _, u := range users {
|
||||||
|
if u.Role == "admin" {
|
||||||
|
adminCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if adminCount <= 1 {
|
||||||
|
respondError(w, http.StatusBadRequest, "cannot demote the last admin user")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Email != "" {
|
||||||
|
existing.Email = req.Email
|
||||||
|
}
|
||||||
|
if req.Role != "" {
|
||||||
|
existing.Role = req.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.UpdateUser(existing); err != nil {
|
||||||
|
slog.Error("failed to update user", "error", err)
|
||||||
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJSON(w, http.StatusOK, existing)
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func (s *Server) Router() chi.Router {
|
|||||||
r.Post("/auth/login", s.rateLimitedLogin(loginLimiter))
|
r.Post("/auth/login", s.rateLimitedLogin(loginLimiter))
|
||||||
r.Get("/auth/oidc/login", s.oidcLogin)
|
r.Get("/auth/oidc/login", s.oidcLogin)
|
||||||
r.Get("/auth/oidc/callback", s.oidcCallback)
|
r.Get("/auth/oidc/callback", s.oidcCallback)
|
||||||
|
r.Post("/auth/oidc/token", s.oidcExchangeToken)
|
||||||
|
|
||||||
// Webhook handler (uses its own secret-based auth).
|
// Webhook handler (uses its own secret-based auth).
|
||||||
r.Mount("/webhook", s.webhook.Route())
|
r.Mount("/webhook", s.webhook.Route())
|
||||||
@@ -115,6 +116,7 @@ func (s *Server) Router() chi.Router {
|
|||||||
|
|
||||||
// Read-only endpoints (any authenticated user).
|
// Read-only endpoints (any authenticated user).
|
||||||
r.Get("/auth/me", s.currentUser)
|
r.Get("/auth/me", s.currentUser)
|
||||||
|
r.Post("/auth/logout", s.logout)
|
||||||
r.Get("/projects", s.listProjects)
|
r.Get("/projects", s.listProjects)
|
||||||
r.Route("/projects/{id}", func(r chi.Router) {
|
r.Route("/projects/{id}", func(r chi.Router) {
|
||||||
r.Get("/", s.getProject)
|
r.Get("/", s.getProject)
|
||||||
@@ -145,6 +147,8 @@ func (s *Server) Router() chi.Router {
|
|||||||
r.Put("/auth/settings", s.updateAuthSettings)
|
r.Put("/auth/settings", s.updateAuthSettings)
|
||||||
r.Get("/auth/users", s.listUsers)
|
r.Get("/auth/users", s.listUsers)
|
||||||
r.Post("/auth/users", s.createUser)
|
r.Post("/auth/users", s.createUser)
|
||||||
|
r.Put("/auth/users/{uid}", s.updateUser)
|
||||||
|
r.Put("/auth/users/{uid}/password", s.changePassword)
|
||||||
r.Delete("/auth/users/{uid}", s.deleteUser)
|
r.Delete("/auth/users/{uid}", s.deleteUser)
|
||||||
|
|
||||||
// Project mutation endpoints.
|
// Project mutation endpoints.
|
||||||
|
|||||||
+43
-1
@@ -3,8 +3,10 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -31,6 +33,8 @@ type jwtClaims struct {
|
|||||||
// LocalAuth handles password hashing and JWT token management for local auth mode.
|
// LocalAuth handles password hashing and JWT token management for local auth mode.
|
||||||
type LocalAuth struct {
|
type LocalAuth struct {
|
||||||
jwtSecret []byte
|
jwtSecret []byte
|
||||||
|
mu sync.RWMutex
|
||||||
|
blacklist map[string]time.Time // token hash -> expiry time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocalAuth creates a LocalAuth deriving the JWT signing key from the encryption key
|
// NewLocalAuth creates a LocalAuth deriving the JWT signing key from the encryption key
|
||||||
@@ -38,8 +42,46 @@ type LocalAuth struct {
|
|||||||
func NewLocalAuth(encKey [32]byte) *LocalAuth {
|
func NewLocalAuth(encKey [32]byte) *LocalAuth {
|
||||||
mac := hmac.New(sha256.New, encKey[:])
|
mac := hmac.New(sha256.New, encKey[:])
|
||||||
mac.Write([]byte("docker-watcher-jwt-secret"))
|
mac.Write([]byte("docker-watcher-jwt-secret"))
|
||||||
return &LocalAuth{
|
la := &LocalAuth{
|
||||||
jwtSecret: mac.Sum(nil),
|
jwtSecret: mac.Sum(nil),
|
||||||
|
blacklist: make(map[string]time.Time),
|
||||||
|
}
|
||||||
|
// Periodically clean expired blacklist entries.
|
||||||
|
go la.cleanBlacklist()
|
||||||
|
return la
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeToken adds a token to the blacklist.
|
||||||
|
func (la *LocalAuth) RevokeToken(tokenString string) {
|
||||||
|
hash := sha256.Sum256([]byte(tokenString))
|
||||||
|
key := hex.EncodeToString(hash[:])
|
||||||
|
la.mu.Lock()
|
||||||
|
defer la.mu.Unlock()
|
||||||
|
la.blacklist[key] = time.Now().Add(TokenExpiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRevoked checks if a token has been revoked.
|
||||||
|
func (la *LocalAuth) IsRevoked(tokenString string) bool {
|
||||||
|
hash := sha256.Sum256([]byte(tokenString))
|
||||||
|
key := hex.EncodeToString(hash[:])
|
||||||
|
la.mu.RLock()
|
||||||
|
defer la.mu.RUnlock()
|
||||||
|
_, exists := la.blacklist[key]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanBlacklist removes expired entries from the blacklist every hour.
|
||||||
|
func (la *LocalAuth) cleanBlacklist() {
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
for range ticker.C {
|
||||||
|
la.mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for k, expiry := range la.blacklist {
|
||||||
|
if now.After(expiry) {
|
||||||
|
delete(la.blacklist, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
la.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const claimsKey contextKey = "auth_claims"
|
|||||||
func Middleware(la *LocalAuth) func(http.Handler) http.Handler {
|
func Middleware(la *LocalAuth) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tokenStr := extractToken(r)
|
tokenStr := ExtractToken(r)
|
||||||
if tokenStr == "" {
|
if tokenStr == "" {
|
||||||
http.Error(w, `{"success":false,"error":"authentication required"}`, http.StatusUnauthorized)
|
http.Error(w, `{"success":false,"error":"authentication required"}`, http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
@@ -30,6 +30,11 @@ func Middleware(la *LocalAuth) func(http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if la.IsRevoked(tokenStr) {
|
||||||
|
http.Error(w, `{"success":false,"error":"token has been revoked"}`, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), claimsKey, claims)
|
ctx := context.WithValue(r.Context(), claimsKey, claims)
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
@@ -55,8 +60,8 @@ func ClaimsFromContext(ctx context.Context) (Claims, bool) {
|
|||||||
return claims, ok
|
return claims, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractToken gets the JWT from the Authorization header or "token" query param.
|
// ExtractToken gets the JWT from the Authorization header or "token" query param.
|
||||||
func extractToken(r *http.Request) string {
|
func ExtractToken(r *http.Request) string {
|
||||||
// Try Authorization: Bearer <token>
|
// Try Authorization: Bearer <token>
|
||||||
authHeader := r.Header.Get("Authorization")
|
authHeader := r.Header.Get("Authorization")
|
||||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
|||||||
Reference in New Issue
Block a user