add a special token type for MCP

This commit is contained in:
MaysWind
2025-07-07 01:20:38 +08:00
parent fbaf6086e3
commit 0140fc7622
26 changed files with 424 additions and 17 deletions
+44 -1
View File
@@ -46,7 +46,7 @@ var (
// TokenListHandler returns available token list of current user
func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
tokens, err := a.tokens.GetAllUnexpiredNormalAndMCPTokensByUid(c, uid)
if err != nil {
log.Errorf(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
@@ -69,6 +69,10 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
tokenResp.IsCurrent = true
}
if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForMCP
}
tokenResps[i] = tokenResp
}
@@ -77,6 +81,45 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
return tokenResps, nil
}
// TokenGenerateMCPHandler generates a new MCP token for current user
func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) {
var generateMCPTokenReq models.TokenGenerateMCPRequest
err := c.ShouldBindJSON(&generateMCPTokenReq)
if err != nil {
log.Warnf(c, "[tokens.TokenGenerateMCPHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(c, uid)
if err != nil {
log.Warnf(c, "[tokens.TokenGenerateMCPHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
if !a.users.IsPasswordEqualsUserPassword(generateMCPTokenReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
token, claims, err := a.tokens.CreateMCPToken(c, user)
if err != nil {
log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
log.Infof(c, "[tokens.TokenGenerateMCPHandler] user \"uid:%d\" has generated mcp token, new token will be expired at %d", user.Uid, claims.ExpiresAt)
generateMCPTokenResp := &models.TokenGenerateMCPResponse{
Token: token,
MCPUrl: a.CurrentConfig().RootUrl + "mcp",
}
return generateMCPTokenResp, nil
}
// TokenRevokeCurrentHandler revokes current token of current user
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Error) {
_, claims, err := a.tokens.ParseTokenByHeader(c)
+12 -3
View File
@@ -394,7 +394,7 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo
return nil, err
}
tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(c, uid)
tokens, err := l.tokens.GetAllUnexpiredNormalAndMCPTokensByUid(c, uid)
if err != nil {
log.CliErrorf(c, "[user_data.ListUserTokens] failed to get tokens of user \"%s\", because %s", username, err.Error())
@@ -405,7 +405,7 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo
}
// CreateNewUserToken returns a new token for the specified user
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string) (*models.TokenRecord, string, error) {
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string) (*models.TokenRecord, string, error) {
if username == "" {
log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty")
return nil, "", errs.ErrUsernameIsEmpty
@@ -418,7 +418,16 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string) (*
return nil, "", err
}
token, tokenRecord, err := l.tokens.CreateTokenViaCli(c, user)
var token string
var tokenRecord *models.TokenRecord
if tokenType == "mcp" {
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user)
} else if tokenType == "normal" {
token, tokenRecord, err = l.tokens.CreateTokenViaCli(c, user)
} else {
return nil, "", errs.ErrParameterInvalid
}
if err != nil {
log.CliErrorf(c, "[user_data.CreateNewUserToken] failed to create token for user \"%s\", because %s", username, err.Error())
+1
View File
@@ -15,6 +15,7 @@ const (
USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2
USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3
USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4
USER_TOKEN_TYPE_MCP TokenType = 5
)
// UserTokenClaims represents user token
+20 -1
View File
@@ -94,6 +94,25 @@ func JWTResetPasswordAuthorization(c *core.WebContext) {
c.Next()
}
// JWTMCPAuthorization verifies whether current request is valid by jwt mcp token in header
func JWTMCPAuthorization(c *core.WebContext) {
claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER)
if err != nil {
utils.PrintJsonErrorResult(c, err)
return
}
if claims.Type != core.USER_TOKEN_TYPE_MCP {
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is not mcp token", claims.Uid, claims.Type)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func jwtAuthorization(c *core.WebContext, source TokenSourceType) {
claims, err := getTokenClaims(c, source)
@@ -109,7 +128,7 @@ func jwtAuthorization(c *core.WebContext, source TokenSourceType) {
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type is invalid", claims.Uid)
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is invalid", claims.Uid, claims.Type)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
}
+11
View File
@@ -17,11 +17,22 @@ type TokenRecord struct {
LastSeenUnixTime int64
}
// TokenGenerateMCPRequest represents all parameters of mcp token generation request
type TokenGenerateMCPRequest struct {
Password string `json:"password" binding:"omitempty,min=6,max=128"`
}
// TokenRevokeRequest represents all parameters of token revoking request
type TokenRevokeRequest struct {
TokenId string `json:"tokenId" binding:"required,notBlank"`
}
// TokenGenerateMCPResponse represents all response parameters of generated mcp token
type TokenGenerateMCPResponse struct {
Token string `json:"token"`
MCPUrl string `json:"mcpUrl"`
}
// TokenRefreshResponse represents all parameters of token refreshing request
type TokenRefreshResponse struct {
NewToken string `json:"newToken,omitempty"`
+26 -4
View File
@@ -19,6 +19,14 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// TokenUserAgentCreatedViaCli is the user agent of token created via cli
const TokenUserAgentCreatedViaCli = "ezbookkeeping Cli"
// TokenUserAgentForMCP is the user agent for MCP token
const TokenUserAgentForMCP = "ezbookkeeping MCP"
const tokenMaxExpiredAtUnixTime = int64(253402300799) // 9999-12-31 23:59:59 UTC
// TokenService represents user token service
type TokenService struct {
ServiceUsingDB
@@ -49,8 +57,8 @@ func (s *TokenService) GetAllTokensByUid(c core.Context, uid int64) ([]*models.T
return tokenRecords, err
}
// GetAllUnexpiredNormalTokensByUid returns all available token models of given user
func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c core.Context, uid int64) ([]*models.TokenRecord, error) {
// GetAllUnexpiredNormalAndMCPTokensByUid returns all available token models of given user
func (s *TokenService) GetAllUnexpiredNormalAndMCPTokensByUid(c core.Context, uid int64) ([]*models.TokenRecord, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
@@ -58,7 +66,7 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c core.Context, uid int6
now := time.Now().Unix()
var tokenRecords []*models.TokenRecord
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords)
err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND (token_type=? OR token_type=?) AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, core.USER_TOKEN_TYPE_MCP, now).Find(&tokenRecords)
return tokenRecords, err
}
@@ -80,7 +88,7 @@ func (s *TokenService) ParseTokenByCookie(c *core.WebContext, tokenCookieName st
// CreateTokenViaCli generates a new normal token and saves to database
func (s *TokenService) CreateTokenViaCli(c *core.CliContext, user *models.User) (string, *models.TokenRecord, error) {
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, "ezbookkeeping Cli", s.CurrentConfig().TokenExpiredTimeDuration)
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, TokenUserAgentCreatedViaCli, s.CurrentConfig().TokenExpiredTimeDuration)
return token, tokenRecord, err
}
@@ -120,6 +128,20 @@ func (s *TokenService) CreatePasswordResetTokenWithoutUserAgent(c core.Context,
return token, claims, err
}
// CreateMCPToken generates a new MCP token and saves to database
func (s *TokenService) CreateMCPToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) {
tokenExpiredTimeDuration := time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, s.getUserAgent(c), tokenExpiredTimeDuration)
return token, claims, err
}
// CreateMCPTokenViaCli generates a new MCP token and saves to database
func (s *TokenService) CreateMCPTokenViaCli(c *core.CliContext, user *models.User) (string, *models.TokenRecord, error) {
tokenExpiredTimeDuration := time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, TokenUserAgentCreatedViaCli, tokenExpiredTimeDuration)
return token, tokenRecord, err
}
// UpdateTokenLastSeen updates the last seen time of specified token
func (s *TokenService) UpdateTokenLastSeen(c core.Context, tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {