From bca7d1ac6e57027f3760ca666aa1933d87e31df1 Mon Sep 17 00:00:00 2001 From: Ryan Copley Date: Fri, 22 Aug 2025 20:53:58 -0400 Subject: [PATCH] - --- cmd/server/main.go | 2 +- internal/services/token_service.go | 170 +++++- kms-frontend/src/App.tsx | 10 + kms-frontend/src/components/TokenTester.tsx | 568 ++++++++++++++++++ .../src/components/TokenTesterCallback.tsx | 379 ++++++++++++ server | Bin 16188263 -> 16330081 bytes 6 files changed, 1113 insertions(+), 16 deletions(-) create mode 100644 kms-frontend/src/components/TokenTester.tsx create mode 100644 kms-frontend/src/components/TokenTesterCallback.tsx diff --git a/cmd/server/main.go b/cmd/server/main.go index 8e97f53..cff6057 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -62,7 +62,7 @@ func main() { // Initialize services appService := services.NewApplicationService(appRepo, logger) - tokenService := services.NewTokenService(tokenRepo, appRepo, permRepo, grantRepo, cfg.GetString("INTERNAL_HMAC_KEY"), logger) + tokenService := services.NewTokenService(tokenRepo, appRepo, permRepo, grantRepo, cfg.GetString("INTERNAL_HMAC_KEY"), cfg, logger) authService := services.NewAuthenticationService(cfg, logger) // Initialize handlers diff --git a/internal/services/token_service.go b/internal/services/token_service.go index 197f933..2378431 100644 --- a/internal/services/token_service.go +++ b/internal/services/token_service.go @@ -8,6 +8,8 @@ import ( "github.com/google/uuid" "go.uber.org/zap" + "github.com/kms/api-key-service/internal/auth" + "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/crypto" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/repository" @@ -20,6 +22,7 @@ type tokenService struct { permRepo repository.PermissionRepository grantRepo repository.GrantedPermissionRepository tokenGen *crypto.TokenGenerator + jwtManager *auth.JWTManager logger *zap.Logger } @@ -30,15 +33,17 @@ func NewTokenService( permRepo repository.PermissionRepository, grantRepo repository.GrantedPermissionRepository, hmacKey string, + config config.ConfigProvider, logger *zap.Logger, ) TokenService { return &tokenService{ - tokenRepo: tokenRepo, - appRepo: appRepo, - permRepo: permRepo, - grantRepo: grantRepo, - tokenGen: crypto.NewTokenGenerator(hmacKey), - logger: logger, + tokenRepo: tokenRepo, + appRepo: appRepo, + permRepo: permRepo, + grantRepo: grantRepo, + tokenGen: crypto.NewTokenGenerator(hmacKey), + jwtManager: auth.NewJWTManager(config, logger), + logger: logger, } } @@ -197,11 +202,57 @@ func (s *tokenService) Delete(ctx context.Context, tokenID uuid.UUID, userID str func (s *tokenService) GenerateUserToken(ctx context.Context, appID, userID string, permissions []string) (string, error) { s.logger.Info("Generating user token", zap.String("app_id", appID), zap.String("user_id", userID)) - // TODO: Validate application - // TODO: Validate permissions - // TODO: Generate JWT token + // Validate application exists + app, err := s.appRepo.GetByID(ctx, appID) + if err != nil { + s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", appID)) + return "", fmt.Errorf("application not found: %w", err) + } - return "user-token-placeholder-" + userID, nil + // Validate permissions exist (if any provided) + var validPermissions []string + if len(permissions) > 0 { + validPermissions, err = s.permRepo.ValidatePermissionScopes(ctx, permissions) + if err != nil { + s.logger.Error("Failed to validate permissions", zap.Error(err)) + return "", fmt.Errorf("failed to validate permissions: %w", err) + } + + if len(validPermissions) != len(permissions) { + s.logger.Warn("Some permissions are invalid", + zap.Strings("requested", permissions), + zap.Strings("valid", validPermissions)) + return "", fmt.Errorf("some requested permissions are invalid") + } + } + + // Create user token with proper timing + now := time.Now() + userToken := &domain.UserToken{ + AppID: appID, + UserID: userID, + Permissions: validPermissions, + IssuedAt: now, + ExpiresAt: now.Add(app.TokenRenewalDuration.Duration), + MaxValidAt: now.Add(app.MaxTokenDuration.Duration), + TokenType: domain.TokenTypeUser, + } + + // Generate JWT token using JWT manager + tokenString, err := s.jwtManager.GenerateToken(userToken) + if err != nil { + s.logger.Error("Failed to generate JWT token", zap.Error(err)) + return "", fmt.Errorf("failed to generate token: %w", err) + } + + s.logger.Info("User token generated successfully", + zap.String("app_id", appID), + zap.String("user_id", userID), + zap.Strings("permissions", validPermissions), + zap.Time("expires_at", userToken.ExpiresAt), + zap.Time("max_valid_at", userToken.MaxValidAt)) + + return tokenString, nil } // VerifyToken verifies a token and returns verification response @@ -338,12 +389,101 @@ func (s *tokenService) verifyStaticToken(ctx context.Context, req *domain.Verify func (s *tokenService) verifyUserToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) { s.logger.Debug("Verifying user token", zap.String("app_id", req.AppID)) - // TODO: Implement JWT token verification - // For now, return an error since user tokens are not fully implemented + // Check if token is revoked first + isRevoked, err := s.jwtManager.IsTokenRevoked(req.Token) + if err != nil { + s.logger.Error("Failed to check token revocation status", zap.Error(err)) + return &domain.VerifyResponse{ + Valid: false, + Permitted: false, + Error: "Token verification failed", + }, nil + } + + if isRevoked { + s.logger.Warn("Token is revoked", zap.String("app_id", req.AppID)) + return &domain.VerifyResponse{ + Valid: false, + Permitted: false, + Error: "Token has been revoked", + }, nil + } + + // Validate JWT token + claims, err := s.jwtManager.ValidateToken(req.Token) + if err != nil { + s.logger.Warn("JWT token validation failed", zap.Error(err), zap.String("app_id", req.AppID)) + return &domain.VerifyResponse{ + Valid: false, + Permitted: false, + Error: "Invalid token", + }, nil + } + + // Verify the token is for the correct application + if claims.AppID != req.AppID { + s.logger.Warn("Token app_id mismatch", + zap.String("expected", req.AppID), + zap.String("actual", claims.AppID)) + return &domain.VerifyResponse{ + Valid: false, + Permitted: false, + Error: "Token not valid for this application", + }, nil + } + + // Check specific permissions if requested + var permissionResults map[string]bool + var permitted bool = true // Default to true if no specific permissions requested + + if len(req.Permissions) > 0 { + permissionResults = make(map[string]bool) + + // Check each requested permission against token permissions + for _, requestedPerm := range req.Permissions { + hasPermission := false + for _, tokenPerm := range claims.Permissions { + if tokenPerm == requestedPerm { + hasPermission = true + break + } + } + permissionResults[requestedPerm] = hasPermission + + // If any permission is missing, set permitted to false + if !hasPermission { + permitted = false + } + } + } + + // Convert timestamps + var expiresAt, maxValidAt *time.Time + if claims.ExpiresAt != nil { + expTime := claims.ExpiresAt.Time + expiresAt = &expTime + } + if claims.MaxValidAt > 0 { + maxTime := time.Unix(claims.MaxValidAt, 0) + maxValidAt = &maxTime + } + + s.logger.Info("User token verified successfully", + zap.String("user_id", claims.UserID), + zap.String("app_id", req.AppID), + zap.Strings("permissions", claims.Permissions), + zap.Bool("permitted", permitted)) + return &domain.VerifyResponse{ - Valid: false, - Permitted: false, - Error: "User token verification not yet implemented", + Valid: true, + Permitted: permitted, + UserID: claims.UserID, + Permissions: claims.Permissions, + PermissionResults: permissionResults, + ExpiresAt: expiresAt, + MaxValidAt: maxValidAt, + TokenType: domain.TokenTypeUser, + Claims: claims.Claims, }, nil } diff --git a/kms-frontend/src/App.tsx b/kms-frontend/src/App.tsx index 3e693f1..8f84ab5 100644 --- a/kms-frontend/src/App.tsx +++ b/kms-frontend/src/App.tsx @@ -8,6 +8,7 @@ import { UserOutlined, AuditOutlined, LoginOutlined, + ExperimentOutlined, } from '@ant-design/icons'; import { useState } from 'react'; import './App.css'; @@ -19,6 +20,8 @@ import Tokens from './components/Tokens'; import Users from './components/Users'; import Audit from './components/Audit'; import Login from './components/Login'; +import TokenTester from './components/TokenTester'; +import TokenTesterCallback from './components/TokenTesterCallback'; import { AuthProvider, useAuth } from './contexts/AuthContext'; const { Header, Sider, Content } = Layout; @@ -50,6 +53,11 @@ const AppContent: React.FC = () => { icon: , label: 'Tokens', }, + { + key: '/token-tester', + icon: , + label: 'Token Tester', + }, { key: '/users', icon: , @@ -103,6 +111,8 @@ const AppContent: React.FC = () => { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/kms-frontend/src/components/TokenTester.tsx b/kms-frontend/src/components/TokenTester.tsx new file mode 100644 index 0000000..d59ca28 --- /dev/null +++ b/kms-frontend/src/components/TokenTester.tsx @@ -0,0 +1,568 @@ +import React, { useState, useEffect } from 'react'; +import { + Card, + Button, + Form, + Input, + Select, + Space, + Typography, + Alert, + Divider, + Row, + Col, + Tag, + Checkbox, + message, + Modal, + Steps, +} from 'antd'; +import { + PlayCircleOutlined, + CheckCircleOutlined, + ExclamationCircleOutlined, + CopyOutlined, + ReloadOutlined, + LinkOutlined, +} from '@ant-design/icons'; +import { apiService, Application } from '../services/apiService'; + +const { Title, Text, Paragraph } = Typography; +const { Option } = Select; +const { TextArea } = Input; +const { Step } = Steps; + +interface LoginTestResult { + success: boolean; + token?: string; + redirectUrl?: string; + userId?: string; + appId?: string; + expiresIn?: number; + error?: string; + timestamp: string; +} + +interface CallbackTestResult { + success: boolean; + token?: string; + verified?: boolean; + permissions?: string[]; + error?: string; + timestamp: string; +} + +const availablePermissions = [ + 'app.read', + 'app.write', + 'app.delete', + 'token.read', + 'token.create', + 'token.revoke', + 'repo.read', + 'repo.write', + 'repo.admin', + 'permission.read', + 'permission.write', + 'permission.grant', + 'permission.revoke', +]; + +const TokenTester: React.FC = () => { + const [applications, setApplications] = useState([]); + const [loading, setLoading] = useState(false); + const [testLoading, setTestLoading] = useState(false); + const [callbackLoading, setCallbackLoading] = useState(false); + const [loginResult, setLoginResult] = useState(null); + const [callbackResult, setCallbackResult] = useState(null); + const [currentStep, setCurrentStep] = useState(0); + const [callbackModalVisible, setCallbackModalVisible] = useState(false); + const [form] = Form.useForm(); + const [callbackForm] = Form.useForm(); + + useEffect(() => { + loadApplications(); + }, []); + + const loadApplications = async () => { + try { + setLoading(true); + const response = await apiService.getApplications(); + setApplications(response.data); + } catch (error) { + console.error('Failed to load applications:', error); + message.error('Failed to load applications'); + } finally { + setLoading(false); + } + }; + + const handleLoginTest = async (values: any) => { + try { + setTestLoading(true); + setCurrentStep(1); + + const selectedApp = applications.find(app => app.app_id === values.app_id); + const callbackUrl = `${window.location.origin}/token-tester/callback`; + + // Store test data in localStorage for the callback page + const testData = { + app_id: values.app_id, + permissions: values.permissions || [], + use_callback: values.use_callback, + timestamp: new Date().toISOString(), + }; + localStorage.setItem('token_tester_data', JSON.stringify(testData)); + + console.log('Testing login flow with:', { + app_id: values.app_id, + permissions: values.permissions || [], + redirect_uri: values.use_callback ? callbackUrl : undefined, + }); + + const response = await apiService.login( + values.app_id, + values.permissions || [], + values.use_callback ? callbackUrl : undefined + ); + + console.log('Login response:', response); + + const result: LoginTestResult = { + success: true, + token: response.token, + redirectUrl: response.redirect_url, + userId: response.user_id, + appId: values.app_id, + expiresIn: response.expires_in, + timestamp: new Date().toISOString(), + }; + + setLoginResult(result); + setCurrentStep(2); + + message.success('Login test completed successfully!'); + + // If we have a redirect URL, show the callback modal + if (response.redirect_url && values.use_callback) { + setCallbackModalVisible(true); + // Pre-fill the callback form with the token from the redirect URL + const urlParams = new URLSearchParams(response.redirect_url.split('?')[1]); + const token = urlParams.get('token'); + if (token) { + callbackForm.setFieldsValue({ + app_id: values.app_id, + token: token, + permissions: values.permissions || [], + }); + } + } + + } catch (error: any) { + console.error('Login test failed:', error); + + const result: LoginTestResult = { + success: false, + error: error.response?.data?.message || error.message || 'Login test failed', + timestamp: new Date().toISOString(), + }; + + setLoginResult(result); + setCurrentStep(2); + message.error('Login test failed'); + } finally { + setTestLoading(false); + } + }; + + const handleCallbackTest = async (values: any) => { + try { + setCallbackLoading(true); + setCurrentStep(3); + + console.log('Testing callback with token verification:', values); + + // Verify the token received in the callback + const verifyResponse = await apiService.verifyToken({ + app_id: values.app_id, + type: 'user', + token: values.token, + permissions: values.permissions || [], + }); + + console.log('Token verification response:', verifyResponse); + + const result: CallbackTestResult = { + success: verifyResponse.valid, + token: values.token, + verified: verifyResponse.valid, + permissions: verifyResponse.permissions, + error: verifyResponse.error, + timestamp: new Date().toISOString(), + }; + + setCallbackResult(result); + setCurrentStep(4); + + if (verifyResponse.valid) { + message.success('Callback test completed successfully!'); + } else { + message.warning('Callback test completed - token verification failed'); + } + + } catch (error: any) { + console.error('Callback test failed:', error); + + const result: CallbackTestResult = { + success: false, + error: error.response?.data?.message || error.message || 'Callback test failed', + timestamp: new Date().toISOString(), + }; + + setCallbackResult(result); + setCurrentStep(4); + message.error('Callback test failed'); + } finally { + setCallbackLoading(false); + } + }; + + const resetTest = () => { + setCurrentStep(0); + setLoginResult(null); + setCallbackResult(null); + setCallbackModalVisible(false); + form.resetFields(); + callbackForm.resetFields(); + // Clear stored test data + localStorage.removeItem('token_tester_data'); + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + message.success('Copied to clipboard'); + }; + + const openCallbackUrl = () => { + if (loginResult?.redirectUrl) { + window.open(loginResult.redirectUrl, '_blank'); + } + }; + + return ( +
+ +
+
+ Token Tester + + Test the /login flow and callback handling for user tokens + +
+ +
+ + {/* Test Progress */} + + + + + + + + + + + {/* Test Configuration */} + +
+ + + + + + + + + Use callback URL (test full flow) + + + + + + + + {availablePermissions.map(permission => ( + + {permission} + + ))} + + + + + + + +
+
+ + {/* Login Test Results */} + {loginResult && ( + + {loginResult.success ? ( + + ) : ( + + )} + Login Test Results +
+ } + > + + + + {loginResult.success && ( +
+ + {loginResult.token && ( + + + +