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
@@ -59,11 +59,14 @@ type loginRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// loginResponse is the login success payload.
|
||||
// loginResponse is the login success payload. RefreshToken is the opaque
|
||||
// long-lived credential (POST /auth/refresh) added by B2; it is omitted when no
|
||||
// refresh store is wired so the pre-B2 contract is unchanged.
|
||||
type loginResponse struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
Role string `json:"role"`
|
||||
Token string `json:"token"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
Role string `json:"role"`
|
||||
RefreshToken string `json:"refreshToken,omitempty"`
|
||||
}
|
||||
|
||||
// chatRequest is the POST /api/v1/chat body. When Stream is true the gateway
|
||||
@@ -114,11 +117,24 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "login_failed", "could not sign token"))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, loginResponse{
|
||||
resp := loginResponse{
|
||||
Token: token,
|
||||
ExpiresAt: time.Now().Add(tokenTTL).Unix(),
|
||||
Role: string(auth.RoleOwner),
|
||||
})
|
||||
}
|
||||
// Issue the long-lived refresh credential alongside the access token. A store
|
||||
// failure fails the login rather than silently handing back a login without
|
||||
// the advertised refresh token; the plaintext is never logged.
|
||||
if s.refresh != nil {
|
||||
refreshToken, _, rerr := s.refresh.Issue(r.Context(), req.Username, auth.RoleOwner, s.refreshTTL())
|
||||
if rerr != nil {
|
||||
s.log.Error("login issue refresh token", "err", rerr)
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "login_failed", "could not issue refresh token"))
|
||||
return
|
||||
}
|
||||
resp.RefreshToken = refreshToken
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// handleChat runs one synchronous agent turn (or triggers streaming).
|
||||
|
||||
Reference in new issue
Block a user