e045e00fbf
Add self-service password change that re-hashes the credential and bumps a per-user session version so all existing access/refresh tokens are rejected. Session version is tracked in Redis with an in-memory fallback and enforced in middleware and refresh. Scope prompt rules and rule groups to a brand: new brand_id column (migrated to a "未归属" fallback brand for existing rows), brand-filtered queries/indexes, and brand-aware admin-web tabs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
146 lines
3.9 KiB
Go
146 lines
3.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/gin-gonic/gin"
|
|
goredis "github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func init() {
|
|
gin.SetMode(gin.TestMode)
|
|
}
|
|
|
|
func setupTestMiddleware(t *testing.T) (*Manager, *SessionStore, *miniredis.Miniredis) {
|
|
t.Helper()
|
|
mr := miniredis.RunT(t)
|
|
rdb := goredis.NewClient(&goredis.Options{Addr: mr.Addr()})
|
|
mgr := NewManager("test-middleware-secret", 15*time.Minute, 7*24*time.Hour)
|
|
sessions := NewSessionStore(rdb)
|
|
return mgr, sessions, mr
|
|
}
|
|
|
|
func TestAuthMiddleware_ValidToken(t *testing.T) {
|
|
mgr, sessions, _ := setupTestMiddleware(t)
|
|
|
|
pair, err := mgr.Issue(1, 10, 100, "editor")
|
|
require.NoError(t, err)
|
|
|
|
r := gin.New()
|
|
r.Use(Middleware(mgr, sessions))
|
|
|
|
var gotActor Actor
|
|
r.GET("/test", func(c *gin.Context) {
|
|
a, ok := ActorFromCtx(c.Request.Context())
|
|
assert.True(t, ok)
|
|
gotActor = a
|
|
c.Status(http.StatusOK)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer "+pair.AccessToken)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, int64(1), gotActor.UserID)
|
|
assert.Equal(t, int64(10), gotActor.TenantID)
|
|
assert.Equal(t, int64(100), gotActor.PrimaryWorkspaceID)
|
|
assert.Equal(t, "editor", gotActor.Role)
|
|
}
|
|
|
|
func TestAuthMiddleware_MissingHeader(t *testing.T) {
|
|
mgr, sessions, _ := setupTestMiddleware(t)
|
|
|
|
r := gin.New()
|
|
r.Use(Middleware(mgr, sessions))
|
|
r.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func TestAuthMiddleware_InvalidToken(t *testing.T) {
|
|
mgr, sessions, _ := setupTestMiddleware(t)
|
|
|
|
r := gin.New()
|
|
r.Use(Middleware(mgr, sessions))
|
|
r.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer bad-token-value")
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func TestAuthMiddleware_RefreshTokenRejected(t *testing.T) {
|
|
mgr, sessions, _ := setupTestMiddleware(t)
|
|
|
|
pair, err := mgr.Issue(1, 10, 100, "editor")
|
|
require.NoError(t, err)
|
|
|
|
r := gin.New()
|
|
r.Use(Middleware(mgr, sessions))
|
|
r.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer "+pair.RefreshToken)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func TestAuthMiddleware_BlacklistedToken(t *testing.T) {
|
|
mgr, sessions, mr := setupTestMiddleware(t)
|
|
|
|
pair, err := mgr.Issue(1, 10, 100, "editor")
|
|
require.NoError(t, err)
|
|
|
|
// Blacklist the access token's JTI
|
|
require.NoError(t, mr.Set(fmt.Sprintf("blacklist:%s", pair.AccessJTI), "1"))
|
|
|
|
r := gin.New()
|
|
r.Use(Middleware(mgr, sessions))
|
|
r.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer "+pair.AccessToken)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
func TestAuthMiddleware_StaleSessionVersionRejected(t *testing.T) {
|
|
mgr, sessions, _ := setupTestMiddleware(t)
|
|
|
|
pair, err := mgr.IssueWithSessionVersion(1, 10, 100, "editor", 0)
|
|
require.NoError(t, err)
|
|
_, err = sessions.BumpSessionVersion(t.Context(), 1)
|
|
require.NoError(t, err)
|
|
|
|
r := gin.New()
|
|
r.Use(Middleware(mgr, sessions))
|
|
r.GET("/test", func(c *gin.Context) { c.Status(http.StatusOK) })
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
req.Header.Set("Authorization", "Bearer "+pair.AccessToken)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
}
|