mirror of https://github.com/cloudreve/Cloudreve
feat(session): sign out and revoke root token
parent
c6ee3e5dcd
commit
db7b54c5d7
2
assets
2
assets
|
@ -1 +1 @@
|
||||||
Subproject commit 5c580559714d6650b3e61dccfcb751adb6cb3db3
|
Subproject commit 59bb76d3fc86fb39e6ad4e3a5f53c3df9092f476
|
|
@ -11,23 +11,27 @@ import (
|
||||||
|
|
||||||
"github.com/cloudreve/Cloudreve/v4/ent"
|
"github.com/cloudreve/Cloudreve/v4/ent"
|
||||||
"github.com/cloudreve/Cloudreve/v4/inventory"
|
"github.com/cloudreve/Cloudreve/v4/inventory"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/cache"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||||
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
"github.com/cloudreve/Cloudreve/v4/pkg/util"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TokenAuth interface {
|
type TokenAuth interface {
|
||||||
// Issue issues a new pair of credentials for the given user.
|
// Issue issues a new pair of credentials for the given user.
|
||||||
Issue(ctx context.Context, u *ent.User) (*Token, error)
|
Issue(ctx context.Context, u *ent.User, rootTokenID *uuid.UUID) (*Token, error)
|
||||||
// VerifyAndRetrieveUser verifies the given token and inject the user into current context.
|
// VerifyAndRetrieveUser verifies the given token and inject the user into current context.
|
||||||
// Returns if upper caller should continue process other session provider.
|
// Returns if upper caller should continue process other session provider.
|
||||||
VerifyAndRetrieveUser(c *gin.Context) (bool, error)
|
VerifyAndRetrieveUser(c *gin.Context) (bool, error)
|
||||||
// Refresh refreshes the given refresh token and returns a new pair of credentials.
|
// Refresh refreshes the given refresh token and returns a new pair of credentials.
|
||||||
Refresh(ctx context.Context, refreshToken string) (*Token, error)
|
Refresh(ctx context.Context, refreshToken string) (*Token, error)
|
||||||
|
// Claims parses the given token string and returns the claims.
|
||||||
|
Claims(ctx context.Context, tokenStr string) (*Claims, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token stores token pair for authentication
|
// Token stores token pair for authentication
|
||||||
|
@ -56,12 +60,14 @@ var (
|
||||||
const (
|
const (
|
||||||
AuthorizationHeader = "Authorization"
|
AuthorizationHeader = "Authorization"
|
||||||
TokenHeaderPrefix = "Bearer "
|
TokenHeaderPrefix = "Bearer "
|
||||||
|
RevokeTokenPrefix = "jwt_revoke_"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
TokenType TokenType `json:"token_type"`
|
TokenType TokenType `json:"token_type"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
StateHash []byte `json:"state_hash,omitempty"`
|
StateHash []byte `json:"state_hash,omitempty"`
|
||||||
|
RootTokenID *uuid.UUID `json:"root_token_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTokenAuth creates a new token based auth provider.
|
// NewTokenAuth creates a new token based auth provider.
|
||||||
|
@ -81,6 +87,24 @@ type tokenAuth struct {
|
||||||
s setting.Provider
|
s setting.Provider
|
||||||
secret []byte
|
secret []byte
|
||||||
userClient inventory.UserClient
|
userClient inventory.UserClient
|
||||||
|
kv cache.Driver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tokenAuth) Claims(ctx context.Context, tokenStr string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return t.secret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid token claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tokenAuth) Refresh(ctx context.Context, refreshToken string) (*Token, error) {
|
func (t *tokenAuth) Refresh(ctx context.Context, refreshToken string) (*Token, error) {
|
||||||
|
@ -113,7 +137,17 @@ func (t *tokenAuth) Refresh(ctx context.Context, refreshToken string) (*Token, e
|
||||||
return nil, ErrInvalidRefreshToken
|
return nil, ErrInvalidRefreshToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.Issue(ctx, expectedUser)
|
// Check if root token is revoked
|
||||||
|
if claims.RootTokenID == nil {
|
||||||
|
return nil, ErrInvalidRefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = t.kv.Get(fmt.Sprintf("%s%s", RevokeTokenPrefix, claims.RootTokenID.String()))
|
||||||
|
if ok {
|
||||||
|
return nil, ErrInvalidRefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Issue(ctx, expectedUser, claims.RootTokenID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tokenAuth) VerifyAndRetrieveUser(c *gin.Context) (bool, error) {
|
func (t *tokenAuth) VerifyAndRetrieveUser(c *gin.Context) (bool, error) {
|
||||||
|
@ -151,12 +185,16 @@ func (t *tokenAuth) VerifyAndRetrieveUser(c *gin.Context) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tokenAuth) Issue(ctx context.Context, u *ent.User) (*Token, error) {
|
func (t *tokenAuth) Issue(ctx context.Context, u *ent.User, rootTokenID *uuid.UUID) (*Token, error) {
|
||||||
uidEncoded := hashid.EncodeUserID(t.idEncoder, u.ID)
|
uidEncoded := hashid.EncodeUserID(t.idEncoder, u.ID)
|
||||||
tokenSettings := t.s.TokenAuth(ctx)
|
tokenSettings := t.s.TokenAuth(ctx)
|
||||||
issueDate := time.Now()
|
issueDate := time.Now()
|
||||||
accessTokenExpired := time.Now().Add(tokenSettings.AccessTokenTTL)
|
accessTokenExpired := time.Now().Add(tokenSettings.AccessTokenTTL)
|
||||||
refreshTokenExpired := time.Now().Add(tokenSettings.RefreshTokenTTL)
|
refreshTokenExpired := time.Now().Add(tokenSettings.RefreshTokenTTL)
|
||||||
|
if rootTokenID == nil {
|
||||||
|
newRootTokenID := uuid.Must(uuid.NewV4())
|
||||||
|
rootTokenID = &newRootTokenID
|
||||||
|
}
|
||||||
|
|
||||||
accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
accessToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
||||||
TokenType: TokenTypeAccess,
|
TokenType: TokenTypeAccess,
|
||||||
|
@ -172,7 +210,8 @@ func (t *tokenAuth) Issue(ctx context.Context, u *ent.User) (*Token, error) {
|
||||||
|
|
||||||
userHash := t.hashUserState(ctx, u)
|
userHash := t.hashUserState(ctx, u)
|
||||||
refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
|
||||||
TokenType: TokenTypeRefresh,
|
TokenType: TokenTypeRefresh,
|
||||||
|
RootTokenID: rootTokenID,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Subject: uidEncoded,
|
Subject: uidEncoded,
|
||||||
NotBefore: jwt.NewNumericDate(issueDate),
|
NotBefore: jwt.NewNumericDate(issueDate),
|
||||||
|
|
|
@ -172,8 +172,17 @@ func UserActivate(c *gin.Context) {
|
||||||
|
|
||||||
// UserSignOut 用户退出登录
|
// UserSignOut 用户退出登录
|
||||||
func UserSignOut(c *gin.Context) {
|
func UserSignOut(c *gin.Context) {
|
||||||
util.DeleteSession(c, "user_id")
|
service := ParametersFromContext[*user.RefreshTokenService](c, user.RefreshTokenParameterCtx{})
|
||||||
c.JSON(200, serializer.Response{})
|
res, err := service.Delete(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(200, serializer.Err(c, err))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, serializer.Response{
|
||||||
|
Data: res,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserMe 获取当前登录的用户
|
// UserMe 获取当前登录的用户
|
||||||
|
|
|
@ -299,6 +299,10 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||||
controllers.FromJSON[usersvc.RefreshTokenService](usersvc.RefreshTokenParameterCtx{}),
|
controllers.FromJSON[usersvc.RefreshTokenService](usersvc.RefreshTokenParameterCtx{}),
|
||||||
controllers.UserRefreshToken,
|
controllers.UserRefreshToken,
|
||||||
)
|
)
|
||||||
|
token.DELETE("",
|
||||||
|
controllers.FromJSON[usersvc.RefreshTokenService](usersvc.RefreshTokenParameterCtx{}),
|
||||||
|
controllers.UserSignOut,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare login
|
// Prepare login
|
||||||
|
@ -1057,8 +1061,6 @@ func initMasterRouter(dep dependency.Dep) *gin.Engine {
|
||||||
controllers.FromQuery[usersvc.SearchUserService](usersvc.SearchUserParamCtx{}),
|
controllers.FromQuery[usersvc.SearchUserService](usersvc.SearchUserParamCtx{}),
|
||||||
controllers.UserSearch,
|
controllers.UserSearch,
|
||||||
)
|
)
|
||||||
// 退出登录
|
|
||||||
user.DELETE("session", controllers.UserSignOut)
|
|
||||||
|
|
||||||
// WebAuthn 注册相关
|
// WebAuthn 注册相关
|
||||||
authn := user.Group("authn",
|
authn := user.Group("authn",
|
||||||
|
|
|
@ -159,7 +159,7 @@ type (
|
||||||
func IssueToken(c *gin.Context) (*BuiltinLoginResponse, error) {
|
func IssueToken(c *gin.Context) (*BuiltinLoginResponse, error) {
|
||||||
dep := dependency.FromContext(c)
|
dep := dependency.FromContext(c)
|
||||||
u := inventory.UserFromContext(c)
|
u := inventory.UserFromContext(c)
|
||||||
token, err := dep.TokenAuth().Issue(c, u)
|
token, err := dep.TokenAuth().Issue(c, u, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, serializer.NewError(serializer.CodeEncryptError, "Failed to issue token pair", err)
|
return nil, serializer.NewError(serializer.CodeEncryptError, "Failed to issue token pair", err)
|
||||||
}
|
}
|
||||||
|
@ -188,6 +188,22 @@ func (s *RefreshTokenService) Refresh(c *gin.Context) (*auth.Token, error) {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RefreshTokenService) Delete(c *gin.Context) (string, error) {
|
||||||
|
dep := dependency.FromContext(c)
|
||||||
|
claims, err := dep.TokenAuth().Claims(c, s.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return "", serializer.NewError(serializer.CodeCredentialInvalid, "Failed to parse token", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block root token
|
||||||
|
if claims.RootTokenID != nil {
|
||||||
|
tokenSettings := dep.SettingProvider().TokenAuth(c)
|
||||||
|
dep.KV().Set(fmt.Sprintf("%s%s", auth.RevokeTokenPrefix, claims.RootTokenID.String()), true, int(tokenSettings.AccessTokenTTL.Seconds()+10))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
OtpValidationParameterCtx struct{}
|
OtpValidationParameterCtx struct{}
|
||||||
OtpValidationService struct {
|
OtpValidationService struct {
|
||||||
|
|
|
@ -70,7 +70,7 @@ func PreparePasskeyLogin(c *gin.Context) (*PreparePasskeyLoginResponse, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := uuid.Must(uuid.NewV4()).String()
|
sessionID := uuid.Must(uuid.NewV4()).String()
|
||||||
if err := dep.KV().Set(fmt.Sprint("%s%s", authnSessionKey, sessionID), *sessionData, 300); err != nil {
|
if err := dep.KV().Set(fmt.Sprintf("%s%s", authnSessionKey, sessionID), *sessionData, 300); err != nil {
|
||||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ func (s *FinishPasskeyLoginService) FinishPasskeyLogin(c *gin.Context) (*ent.Use
|
||||||
kv := dep.KV()
|
kv := dep.KV()
|
||||||
userClient := dep.UserClient()
|
userClient := dep.UserClient()
|
||||||
|
|
||||||
sessionDataRaw, ok := kv.Get(fmt.Sprint("%s%s", authnSessionKey, s.SessionID))
|
sessionDataRaw, ok := kv.Get(fmt.Sprintf("%s%s", authnSessionKey, s.SessionID))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||||
}
|
}
|
||||||
|
@ -192,7 +192,7 @@ func PreparePasskeyRegister(c *gin.Context) (*protocol.CredentialCreation, error
|
||||||
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
return nil, serializer.NewError(serializer.CodeInitializeAuthn, "Failed to begin registration", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dep.KV().Set(fmt.Sprint("%s%d", authnSessionKey, u.ID), *sessionData, 300); err != nil {
|
if err := dep.KV().Set(fmt.Sprintf("%s%d", authnSessionKey, u.ID), *sessionData, 300); err != nil {
|
||||||
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
return nil, serializer.NewError(serializer.CodeInternalSetting, "Failed to store session data", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ func (s *FinishPasskeyRegisterService) FinishPasskeyRegister(c *gin.Context) (*P
|
||||||
kv := dep.KV()
|
kv := dep.KV()
|
||||||
u := inventory.UserFromContext(c)
|
u := inventory.UserFromContext(c)
|
||||||
|
|
||||||
sessionDataRaw, ok := kv.Get(fmt.Sprint("%s%d", authnSessionKey, u.ID))
|
sessionDataRaw, ok := kv.Get(fmt.Sprintf("%s%d", authnSessionKey, u.ID))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
return nil, serializer.NewError(serializer.CodeNotFound, "Session not found", nil)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue