diff --git a/routes/routes.go b/routes/routes.go index 0e7e439..4f2c720 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -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) { diff --git a/services/tokens.go b/services/tokens.go index 3471105..50cf16b 100644 --- a/services/tokens.go +++ b/services/tokens.go @@ -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 +}