2
0

feat(vault): add token introspection endpoint
Some checks failed
Build and Release / Trigger Server Rebuild (push) Has been skipped
Build and Release / Tests (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Keygen Utility (arm64, linux) (push) Has been skipped
Build and Release / Build Keygen Utility (amd64, darwin) (push) Has been skipped
Build and Release / Build Keygen Utility (amd64, linux) (push) Has been skipped
Build and Release / Build Keygen Utility (amd64, windows) (push) Has been skipped
Build and Release / Build Keygen Utility (arm64, darwin) (push) Has been skipped

Add GET /token/info API endpoint for vault token introspection. Allows clients to validate tokens and retrieve scope, permissions, and expiration info. Implement GetTokenInfo service method that validates tokens without checking specific permissions.
This commit is contained in:
2026-01-21 20:21:12 -05:00
parent 06867c4a20
commit 7f627fac0d
2 changed files with 121 additions and 0 deletions

View File

@@ -99,6 +99,16 @@ type AuditEntryResponse struct {
Timestamp int64 `json:"timestamp"`
}
// TokenInfoResponse represents token information for introspection
type TokenInfoResponse struct {
Scope string `json:"scope"`
Description string `json:"description,omitempty"`
ExpiresAt int64 `json:"expires_at,omitempty"`
CanRead bool `json:"can_read"`
CanWrite bool `json:"can_write"`
IsAdmin bool `json:"is_admin"`
}
// API Request types
// CreateSecretRequest is the request body for creating a secret
@@ -182,6 +192,9 @@ func RegisterRepoAPIRoutes(r plugins.PluginRouter, lic *license.Manager) {
r.Post("/tokens", apiCreateToken(lic))
r.Delete("/tokens/{id}", apiRevokeToken(lic))
// Token introspection (uses Bearer auth)
r.Get("/token/info", apiGetTokenInfo(lic))
// Key rotation (enterprise)
r.Post("/rotate-key", apiRotateKey(lic))
})
@@ -884,6 +897,84 @@ func apiRevokeToken(lic *license.Manager) http.HandlerFunc {
}
}
func apiGetTokenInfo(lic *license.Manager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !requireWebLicense(lic, r) {
return
}
ctx := getRepoContext(r)
if ctx == nil || ctx.Repo.Repository == nil {
http.Error(w, "Repository context not found", http.StatusInternalServerError)
return
}
// Get token from Authorization header
auth := r.Header.Get("Authorization")
if auth == "" {
ctx.JSON(http.StatusUnauthorized, map[string]any{
"error": "unauthorized",
"message": "Authorization header required",
})
return
}
// Support "Bearer gvt_xxx" or just "gvt_xxx"
rawToken := auth
if strings.HasPrefix(auth, "Bearer ") {
rawToken = strings.TrimPrefix(auth, "Bearer ")
} else if strings.HasPrefix(auth, "token ") {
rawToken = strings.TrimPrefix(auth, "token ")
}
// Validate the token without checking permissions
token, err := services.GetTokenInfo(ctx, rawToken)
if err != nil {
switch err {
case services.ErrInvalidToken:
ctx.JSON(http.StatusUnauthorized, map[string]any{
"error": "invalid_token",
"message": "Invalid vault token",
})
case services.ErrTokenExpired:
ctx.JSON(http.StatusUnauthorized, map[string]any{
"error": "token_expired",
"message": "Vault token has expired",
})
case services.ErrTokenRevoked:
ctx.JSON(http.StatusUnauthorized, map[string]any{
"error": "token_revoked",
"message": "Vault token has been revoked",
})
default:
ctx.JSON(http.StatusInternalServerError, map[string]any{
"error": "internal_error",
"message": err.Error(),
})
}
return
}
// Verify token is for this repo
if token.RepoID != ctx.Repo.Repository.ID {
ctx.JSON(http.StatusForbidden, map[string]any{
"error": "forbidden",
"message": "Token not valid for this repository",
})
return
}
ctx.JSON(http.StatusOK, TokenInfoResponse{
Scope: string(token.Scope),
Description: token.Description,
ExpiresAt: int64(token.ExpiresUnix),
CanRead: token.Scope.CanRead("*"),
CanWrite: token.Scope.CanWrite("*"),
IsAdmin: token.Scope.IsAdmin(),
})
}
}
func apiRotateKey(lic *license.Manager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !requireWebLicense(lic, r) {

View File

@@ -148,3 +148,33 @@ func ValidateToken(ctx context.Context, rawToken string, action string, secretNa
return token, nil
}
// GetTokenInfo returns token information without checking permissions
// This is used for token introspection by clients
func GetTokenInfo(ctx context.Context, rawToken string) (*models.VaultToken, error) {
// Hash the provided token
hash := sha256.Sum256([]byte(rawToken))
hashedToken := hex.EncodeToString(hash[:])
// Find the token
token := &models.VaultToken{}
has, err := db.GetEngine(ctx).Where("token_hash = ?", hashedToken).Get(token)
if err != nil {
return nil, err
}
if !has {
return nil, ErrInvalidToken
}
// Check if revoked
if token.IsRevoked() {
return nil, ErrTokenRevoked
}
// Check if expired
if token.IsExpired() {
return nil, ErrTokenExpired
}
return token, nil
}