feat(auth): refresh token 续期(B2) — 不透明哈希+单次旋转+/auth/refresh+/auth/logout
- auth/refresh.go: RefreshStore(Issue/Rotate/Revoke/RevokeAll/RevokeByUser), SHA-256哈希256-bit随机串, 单次旋转事务(并发重放fail-closed) - 迁移0008_refresh_tokens(id/user_id/role/token_hash UNIQUE/expires_at/revoked_at + 索引) - gateway: POST /v1/auth/refresh(旋转/续期) + /v1/auth/logout(单/全设备); login响应增refreshToken - config: AUTH_REFRESH_TTL(720h)/AUTH_REFRESH_ROTATE_DISABLED; decode.go 支持time.Duration - rotate-jwt成功后RevokeAll(响应refreshTokensRevoked), revoke-keys不动refresh - 14测试+真实二进制e2e, build/vet/test绿+交叉编译 - Docs/api-client v0.25 + api-reference §2.3/2.4 同步
This commit is contained in:
1 parent
7ecb43bd5d
commit
026b27b310
17 files changed
+1237
-13
No files matched your search
@@ -157,6 +157,32 @@ type UserStore interface {
|
||||
List() []auth.UserSummary
|
||||
}
|
||||
|
||||
// RefreshGrantStore manages opaque refresh tokens (auth.RefreshStore is the
|
||||
// production implementation). It is the credential behind the refreshToken
|
||||
// returned by login and the POST /auth/refresh, /auth/logout endpoints. Only
|
||||
// hashes are persisted; the plaintext token crosses this interface exactly once
|
||||
// per issue/rotate. Optional: when nil login omits refreshToken and the refresh
|
||||
// and logout endpoints report a clear not-configured error.
|
||||
type RefreshGrantStore interface {
|
||||
// Issue mints a refresh token for userID/role and returns its plaintext
|
||||
// (visible once) plus its expiry.
|
||||
Issue(ctx context.Context, userID string, role auth.Role, ttl time.Duration) (string, time.Time, error)
|
||||
// Rotate validates plaintext and, when opts.Rotate is set, revokes it and
|
||||
// returns a replacement. It returns auth.ErrInvalidGrant for an unknown,
|
||||
// revoked, or expired token.
|
||||
Rotate(ctx context.Context, plaintext string, opts auth.RotateOptions) (auth.RefreshToken, string, error)
|
||||
// Revoke marks the token matching plaintext revoked, returning whether a
|
||||
// live token was found. It is idempotent.
|
||||
Revoke(ctx context.Context, plaintext string) (bool, error)
|
||||
// RevokeAll deletes every refresh token (global invalidation on secret
|
||||
// rotation) and returns how many were removed.
|
||||
RevokeAll(ctx context.Context) (int64, error)
|
||||
// RevokeByUser deletes every refresh token for userID (logout by access
|
||||
// token, where the individual grant is unknown) and returns how many were
|
||||
// removed.
|
||||
RevokeByUser(ctx context.Context, userID string) (int64, error)
|
||||
}
|
||||
|
||||
// BackthinkStore enumerates the persisted background-think records for the
|
||||
// M9/M10 monitor panel. *backthink.Backthink is the production implementation.
|
||||
type BackthinkStore interface {
|
||||
@@ -247,6 +273,9 @@ type Deps struct {
|
||||
// Users lists accounts and API keys. Optional (falls back to the configured
|
||||
// single admin account).
|
||||
Users UserStore
|
||||
// Refresh is the opaque refresh-token store. Optional: when nil login omits
|
||||
// refreshToken and /auth/refresh + /auth/logout answer not-configured.
|
||||
Refresh RefreshGrantStore
|
||||
// Backthink lists background-think records. Optional.
|
||||
Backthink BackthinkStore
|
||||
// RAG backs the admin knowledge-base RAG module. Optional; list/search
|
||||
@@ -314,6 +343,7 @@ type Server struct {
|
||||
taskOutputs TaskOutputStore
|
||||
automations AutomationStore
|
||||
users UserStore
|
||||
refresh RefreshGrantStore
|
||||
backthink BackthinkStore
|
||||
rag RagStore
|
||||
obs *observability
|
||||
@@ -363,6 +393,7 @@ func New(cfg *config.Config, deps Deps) (*Server, error) {
|
||||
taskOutputs: deps.Tasks,
|
||||
automations: deps.Automations,
|
||||
users: deps.Users,
|
||||
refresh: deps.Refresh,
|
||||
backthink: deps.Backthink,
|
||||
rag: deps.RAG,
|
||||
obs: newObservability(deps),
|
||||
@@ -390,6 +421,11 @@ func (s *Server) Handler() http.Handler {
|
||||
// Public, no auth.
|
||||
mux.HandleFunc("GET /api/v1/health", s.handleHealth)
|
||||
mux.HandleFunc("POST /api/v1/auth/login", s.handleLogin)
|
||||
// Refresh/logout are public by design: the opaque refresh token is itself the
|
||||
// credential, so a client with an expired access token can still rotate or
|
||||
// revoke it. Both fail closed with 401 invalid_grant on a bad token.
|
||||
mux.HandleFunc("POST /api/v1/auth/refresh", s.handleRefresh)
|
||||
mux.HandleFunc("POST /api/v1/auth/logout", s.handleLogout)
|
||||
|
||||
// User side, authenticated. Chat is a write (owner/admin); reads accept any
|
||||
// authenticated role so a viewer can still list personas/sessions.
|
||||
|
||||
Reference in new issue
Block a user