-
This commit is contained in:
@ -62,7 +62,7 @@ func main() {
|
|||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
appService := services.NewApplicationService(appRepo, logger)
|
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)
|
authService := services.NewAuthenticationService(cfg, logger)
|
||||||
|
|
||||||
// Initialize handlers
|
// Initialize handlers
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"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/crypto"
|
||||||
"github.com/kms/api-key-service/internal/domain"
|
"github.com/kms/api-key-service/internal/domain"
|
||||||
"github.com/kms/api-key-service/internal/repository"
|
"github.com/kms/api-key-service/internal/repository"
|
||||||
@ -20,6 +22,7 @@ type tokenService struct {
|
|||||||
permRepo repository.PermissionRepository
|
permRepo repository.PermissionRepository
|
||||||
grantRepo repository.GrantedPermissionRepository
|
grantRepo repository.GrantedPermissionRepository
|
||||||
tokenGen *crypto.TokenGenerator
|
tokenGen *crypto.TokenGenerator
|
||||||
|
jwtManager *auth.JWTManager
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,15 +33,17 @@ func NewTokenService(
|
|||||||
permRepo repository.PermissionRepository,
|
permRepo repository.PermissionRepository,
|
||||||
grantRepo repository.GrantedPermissionRepository,
|
grantRepo repository.GrantedPermissionRepository,
|
||||||
hmacKey string,
|
hmacKey string,
|
||||||
|
config config.ConfigProvider,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) TokenService {
|
) TokenService {
|
||||||
return &tokenService{
|
return &tokenService{
|
||||||
tokenRepo: tokenRepo,
|
tokenRepo: tokenRepo,
|
||||||
appRepo: appRepo,
|
appRepo: appRepo,
|
||||||
permRepo: permRepo,
|
permRepo: permRepo,
|
||||||
grantRepo: grantRepo,
|
grantRepo: grantRepo,
|
||||||
tokenGen: crypto.NewTokenGenerator(hmacKey),
|
tokenGen: crypto.NewTokenGenerator(hmacKey),
|
||||||
logger: logger,
|
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) {
|
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))
|
s.logger.Info("Generating user token", zap.String("app_id", appID), zap.String("user_id", userID))
|
||||||
|
|
||||||
// TODO: Validate application
|
// Validate application exists
|
||||||
// TODO: Validate permissions
|
app, err := s.appRepo.GetByID(ctx, appID)
|
||||||
// TODO: Generate JWT token
|
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
|
// 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) {
|
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))
|
s.logger.Debug("Verifying user token", zap.String("app_id", req.AppID))
|
||||||
|
|
||||||
// TODO: Implement JWT token verification
|
// Check if token is revoked first
|
||||||
// For now, return an error since user tokens are not fully implemented
|
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{
|
return &domain.VerifyResponse{
|
||||||
Valid: false,
|
Valid: true,
|
||||||
Permitted: false,
|
Permitted: permitted,
|
||||||
Error: "User token verification not yet implemented",
|
UserID: claims.UserID,
|
||||||
|
Permissions: claims.Permissions,
|
||||||
|
PermissionResults: permissionResults,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
MaxValidAt: maxValidAt,
|
||||||
|
TokenType: domain.TokenTypeUser,
|
||||||
|
Claims: claims.Claims,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
UserOutlined,
|
UserOutlined,
|
||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
LoginOutlined,
|
LoginOutlined,
|
||||||
|
ExperimentOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@ -19,6 +20,8 @@ import Tokens from './components/Tokens';
|
|||||||
import Users from './components/Users';
|
import Users from './components/Users';
|
||||||
import Audit from './components/Audit';
|
import Audit from './components/Audit';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
|
import TokenTester from './components/TokenTester';
|
||||||
|
import TokenTesterCallback from './components/TokenTesterCallback';
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
@ -50,6 +53,11 @@ const AppContent: React.FC = () => {
|
|||||||
icon: <KeyOutlined />,
|
icon: <KeyOutlined />,
|
||||||
label: 'Tokens',
|
label: 'Tokens',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '/token-tester',
|
||||||
|
icon: <ExperimentOutlined />,
|
||||||
|
label: 'Token Tester',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '/users',
|
key: '/users',
|
||||||
icon: <UserOutlined />,
|
icon: <UserOutlined />,
|
||||||
@ -103,6 +111,8 @@ const AppContent: React.FC = () => {
|
|||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/applications" element={<Applications />} />
|
<Route path="/applications" element={<Applications />} />
|
||||||
<Route path="/tokens" element={<Tokens />} />
|
<Route path="/tokens" element={<Tokens />} />
|
||||||
|
<Route path="/token-tester" element={<TokenTester />} />
|
||||||
|
<Route path="/token-tester/callback" element={<TokenTesterCallback />} />
|
||||||
<Route path="/users" element={<Users />} />
|
<Route path="/users" element={<Users />} />
|
||||||
<Route path="/audit" element={<Audit />} />
|
<Route path="/audit" element={<Audit />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
568
kms-frontend/src/components/TokenTester.tsx
Normal file
568
kms-frontend/src/components/TokenTester.tsx
Normal file
@ -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<Application[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [testLoading, setTestLoading] = useState(false);
|
||||||
|
const [callbackLoading, setCallbackLoading] = useState(false);
|
||||||
|
const [loginResult, setLoginResult] = useState<LoginTestResult | null>(null);
|
||||||
|
const [callbackResult, setCallbackResult] = useState<CallbackTestResult | null>(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 (
|
||||||
|
<div>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<Title level={2}>Token Tester</Title>
|
||||||
|
<Text type="secondary">
|
||||||
|
Test the /login flow and callback handling for user tokens
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={resetTest}
|
||||||
|
disabled={testLoading || callbackLoading}
|
||||||
|
>
|
||||||
|
Reset Test
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Progress */}
|
||||||
|
<Card title="Test Progress">
|
||||||
|
<Steps current={currentStep} size="small">
|
||||||
|
<Step title="Configure" description="Set up test parameters" />
|
||||||
|
<Step title="Login Test" description="Test /login endpoint" />
|
||||||
|
<Step title="Results" description="Review login results" />
|
||||||
|
<Step title="Callback Test" description="Test callback handling" />
|
||||||
|
<Step title="Complete" description="Test completed" />
|
||||||
|
</Steps>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Test Configuration */}
|
||||||
|
<Card title="Test Configuration">
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleLoginTest}
|
||||||
|
>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="app_id"
|
||||||
|
label="Application"
|
||||||
|
rules={[{ required: true, message: 'Please select an application' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="Select application to test"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{applications.map(app => (
|
||||||
|
<Option key={app.app_id} value={app.app_id}>
|
||||||
|
<div>
|
||||||
|
<Text strong>{app.app_id}</Text>
|
||||||
|
<br />
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||||
|
{app.app_link}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="use_callback"
|
||||||
|
valuePropName="checked"
|
||||||
|
label=" "
|
||||||
|
>
|
||||||
|
<Checkbox>Use callback URL (test full flow)</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="permissions"
|
||||||
|
label="Permissions to Request"
|
||||||
|
>
|
||||||
|
<Checkbox.Group>
|
||||||
|
<Row>
|
||||||
|
{availablePermissions.map(permission => (
|
||||||
|
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
|
||||||
|
<Checkbox value={permission}>{permission}</Checkbox>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
icon={<PlayCircleOutlined />}
|
||||||
|
loading={testLoading}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
Start Login Test
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Login Test Results */}
|
||||||
|
{loginResult && (
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{loginResult.success ? (
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
) : (
|
||||||
|
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||||
|
)}
|
||||||
|
Login Test Results
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
message={loginResult.success ? 'Login Test Successful' : 'Login Test Failed'}
|
||||||
|
description={loginResult.success
|
||||||
|
? 'The /login endpoint responded successfully'
|
||||||
|
: loginResult.error
|
||||||
|
}
|
||||||
|
type={loginResult.success ? 'success' : 'error'}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
{loginResult.success && (
|
||||||
|
<div>
|
||||||
|
<Row gutter={16}>
|
||||||
|
{loginResult.token && (
|
||||||
|
<Col span={12}>
|
||||||
|
<Card size="small" title="User Token">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<TextArea
|
||||||
|
value={loginResult.token}
|
||||||
|
readOnly
|
||||||
|
rows={3}
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: '12px' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => copyToClipboard(loginResult.token!)}
|
||||||
|
>
|
||||||
|
Copy Token
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loginResult.redirectUrl && (
|
||||||
|
<Col span={12}>
|
||||||
|
<Card size="small" title="Redirect URL">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Text code style={{ fontSize: '12px', wordBreak: 'break-all' }}>
|
||||||
|
{loginResult.redirectUrl}
|
||||||
|
</Text>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => copyToClipboard(loginResult.redirectUrl!)}
|
||||||
|
>
|
||||||
|
Copy URL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<LinkOutlined />}
|
||||||
|
onClick={openCallbackUrl}
|
||||||
|
>
|
||||||
|
Open URL
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={6}>
|
||||||
|
<Text strong>User ID:</Text>
|
||||||
|
<div>{loginResult.userId || 'N/A'}</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Text strong>App ID:</Text>
|
||||||
|
<div>{loginResult.appId}</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Text strong>Expires In:</Text>
|
||||||
|
<div>{loginResult.expiresIn ? `${loginResult.expiresIn}s` : 'N/A'}</div>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Text strong>Timestamp:</Text>
|
||||||
|
<div>{new Date(loginResult.timestamp).toLocaleString()}</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Callback Test Results */}
|
||||||
|
{callbackResult && (
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{callbackResult.success ? (
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
) : (
|
||||||
|
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||||
|
)}
|
||||||
|
Callback Test Results
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
message={callbackResult.success ? 'Callback Test Successful' : 'Callback Test Failed'}
|
||||||
|
description={callbackResult.success
|
||||||
|
? 'Token verification in callback was successful'
|
||||||
|
: callbackResult.error
|
||||||
|
}
|
||||||
|
type={callbackResult.success ? 'success' : 'error'}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
{callbackResult.success && callbackResult.permissions && (
|
||||||
|
<div>
|
||||||
|
<Text strong>Verified Permissions:</Text>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
{callbackResult.permissions.map(permission => (
|
||||||
|
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
||||||
|
{permission}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>Timestamp:</Text>
|
||||||
|
<div>{new Date(callbackResult.timestamp).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{/* Callback Test Modal */}
|
||||||
|
<Modal
|
||||||
|
title="Test Callback Handling"
|
||||||
|
open={callbackModalVisible}
|
||||||
|
onCancel={() => setCallbackModalVisible(false)}
|
||||||
|
onOk={() => callbackForm.submit()}
|
||||||
|
confirmLoading={callbackLoading}
|
||||||
|
width={700}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
message="Callback URL Received"
|
||||||
|
description="The login flow returned a redirect URL with a token. Test the callback handling by verifying the token."
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
form={callbackForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleCallbackTest}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="app_id"
|
||||||
|
label="Application ID"
|
||||||
|
>
|
||||||
|
<Input readOnly />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="token"
|
||||||
|
label="Token from Callback"
|
||||||
|
rules={[{ required: true, message: 'Token is required' }]}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={3}
|
||||||
|
style={{ fontFamily: 'monospace' }}
|
||||||
|
placeholder="Token extracted from callback URL"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="permissions"
|
||||||
|
label="Permissions to Verify"
|
||||||
|
>
|
||||||
|
<Checkbox.Group disabled>
|
||||||
|
<Row>
|
||||||
|
{availablePermissions.map(permission => (
|
||||||
|
<Col span={8} key={permission} style={{ marginBottom: '8px' }}>
|
||||||
|
<Checkbox value={permission}>{permission}</Checkbox>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TokenTester;
|
||||||
379
kms-frontend/src/components/TokenTesterCallback.tsx
Normal file
379
kms-frontend/src/components/TokenTesterCallback.tsx
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
Alert,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Tag,
|
||||||
|
Spin,
|
||||||
|
Result,
|
||||||
|
Input,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { apiService } from '../services/apiService';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface CallbackData {
|
||||||
|
token?: string;
|
||||||
|
state?: string;
|
||||||
|
error?: string;
|
||||||
|
error_description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VerificationResult {
|
||||||
|
valid: boolean;
|
||||||
|
permitted: boolean;
|
||||||
|
user_id?: string;
|
||||||
|
permissions: string[];
|
||||||
|
permission_results?: Record<string, boolean>;
|
||||||
|
expires_at?: string;
|
||||||
|
max_valid_at?: string;
|
||||||
|
token_type: string;
|
||||||
|
claims?: Record<string, string>;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TokenTesterCallback: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [callbackData, setCallbackData] = useState<CallbackData>({});
|
||||||
|
const [verificationResult, setVerificationResult] = useState<VerificationResult | null>(null);
|
||||||
|
const [verificationError, setVerificationError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseCallbackData();
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
const parseCallbackData = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Parse URL parameters
|
||||||
|
const urlParams = new URLSearchParams(location.search);
|
||||||
|
const data: CallbackData = {
|
||||||
|
token: urlParams.get('token') || undefined,
|
||||||
|
state: urlParams.get('state') || undefined,
|
||||||
|
error: urlParams.get('error') || undefined,
|
||||||
|
error_description: urlParams.get('error_description') || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
setCallbackData(data);
|
||||||
|
|
||||||
|
// If we have a token, try to verify it
|
||||||
|
if (data.token && !data.error) {
|
||||||
|
await verifyToken(data.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing callback data:', error);
|
||||||
|
setVerificationError('Failed to parse callback data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = async (token: string) => {
|
||||||
|
try {
|
||||||
|
// We need to extract app_id from the state or make a best guess
|
||||||
|
// For now, we'll try to verify without specifying app_id
|
||||||
|
// In a real implementation, the app_id should be included in the state parameter
|
||||||
|
|
||||||
|
// Try to get app_id from localStorage if it was stored during the test
|
||||||
|
const testData = localStorage.getItem('token_tester_data');
|
||||||
|
let appId = '';
|
||||||
|
|
||||||
|
if (testData) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(testData);
|
||||||
|
appId = parsed.app_id || '';
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not parse stored test data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appId) {
|
||||||
|
// If we don't have app_id, we can't verify the token properly
|
||||||
|
setVerificationError('Cannot verify token: Application ID not found in callback state');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyRequest = {
|
||||||
|
app_id: appId,
|
||||||
|
type: 'user',
|
||||||
|
token: token,
|
||||||
|
permissions: [], // We'll verify without specific permissions
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await apiService.verifyToken(verifyRequest);
|
||||||
|
setVerificationResult(result);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Token verification failed:', error);
|
||||||
|
setVerificationError(
|
||||||
|
error.response?.data?.message ||
|
||||||
|
error.message ||
|
||||||
|
'Token verification failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text: string) => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBackToTester = () => {
|
||||||
|
navigate('/token-tester');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: '400px'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" />
|
||||||
|
<Text style={{ marginLeft: '16px' }}>Processing callback...</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<Title level={2}>Token Tester - Callback</Title>
|
||||||
|
<Text type="secondary">
|
||||||
|
Callback page for testing the login flow
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={goBackToTester}
|
||||||
|
>
|
||||||
|
Back to Tester
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Callback Status */}
|
||||||
|
<Card title="Callback Status">
|
||||||
|
{callbackData.error ? (
|
||||||
|
<Alert
|
||||||
|
message="Callback Error"
|
||||||
|
description={`${callbackData.error}: ${callbackData.error_description || 'No description provided'}`}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
) : callbackData.token ? (
|
||||||
|
<Alert
|
||||||
|
message="Callback Successful"
|
||||||
|
description="Token received successfully from the login flow"
|
||||||
|
type="success"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
message="Invalid Callback"
|
||||||
|
description="No token or error information found in callback URL"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Token Information */}
|
||||||
|
{callbackData.token && (
|
||||||
|
<Card title="Received Token">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<Text strong>Token:</Text>
|
||||||
|
<TextArea
|
||||||
|
value={callbackData.token}
|
||||||
|
readOnly
|
||||||
|
rows={4}
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: '12px', marginTop: '8px' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => copyToClipboard(callbackData.token!)}
|
||||||
|
style={{ marginTop: '8px' }}
|
||||||
|
>
|
||||||
|
Copy Token
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{callbackData.state && (
|
||||||
|
<div>
|
||||||
|
<Text strong>State:</Text>
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
<Text code>{callbackData.state}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Token Verification Results */}
|
||||||
|
{verificationResult && (
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
{verificationResult.valid ? (
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
) : (
|
||||||
|
<ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||||
|
)}
|
||||||
|
Token Verification Results
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<Alert
|
||||||
|
message={verificationResult.valid ? 'Token Valid' : 'Token Invalid'}
|
||||||
|
description={verificationResult.valid
|
||||||
|
? 'The token was successfully verified'
|
||||||
|
: verificationResult.error || 'Token verification failed'
|
||||||
|
}
|
||||||
|
type={verificationResult.valid ? 'success' : 'error'}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
|
||||||
|
{verificationResult.valid && (
|
||||||
|
<div>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card size="small" title="Token Information">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<Text strong>Token Type:</Text>
|
||||||
|
<div>
|
||||||
|
<Tag color="blue">{verificationResult.token_type.toUpperCase()}</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{verificationResult.user_id && (
|
||||||
|
<div>
|
||||||
|
<Text strong>User ID:</Text>
|
||||||
|
<div>{verificationResult.user_id}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{verificationResult.expires_at && (
|
||||||
|
<div>
|
||||||
|
<Text strong>Expires At:</Text>
|
||||||
|
<div>{new Date(verificationResult.expires_at).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{verificationResult.max_valid_at && (
|
||||||
|
<div>
|
||||||
|
<Text strong>Max Valid Until:</Text>
|
||||||
|
<div>{new Date(verificationResult.max_valid_at).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={12}>
|
||||||
|
<Card size="small" title="Permissions">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{verificationResult.permissions && verificationResult.permissions.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<Text strong>Available Permissions:</Text>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
{verificationResult.permissions.map(permission => (
|
||||||
|
<Tag key={permission} color="green" style={{ margin: '2px' }}>
|
||||||
|
{permission}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">No permissions available</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{verificationResult.permission_results && Object.keys(verificationResult.permission_results).length > 0 && (
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<Text strong>Permission Check Results:</Text>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
{Object.entries(verificationResult.permission_results).map(([permission, granted]) => (
|
||||||
|
<div key={permission} style={{ marginBottom: '4px' }}>
|
||||||
|
<Tag color={granted ? 'green' : 'red'}>
|
||||||
|
{permission}: {granted ? 'GRANTED' : 'DENIED'}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{verificationResult.claims && Object.keys(verificationResult.claims).length > 0 && (
|
||||||
|
<Card size="small" title="Token Claims" style={{ marginTop: '16px' }}>
|
||||||
|
<Row gutter={8}>
|
||||||
|
{Object.entries(verificationResult.claims).map(([key, value]) => (
|
||||||
|
<Col span={8} key={key} style={{ marginBottom: '8px' }}>
|
||||||
|
<Text strong>{key}:</Text>
|
||||||
|
<div>{value}</div>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Verification Error */}
|
||||||
|
{verificationError && (
|
||||||
|
<Card title="Verification Error">
|
||||||
|
<Alert
|
||||||
|
message="Token Verification Failed"
|
||||||
|
description={verificationError}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Token or Error */}
|
||||||
|
{!callbackData.token && !callbackData.error && (
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title="Invalid Callback"
|
||||||
|
subTitle="This callback page expects to receive either a token or error information from the login flow."
|
||||||
|
extra={
|
||||||
|
<Button type="primary" onClick={goBackToTester}>
|
||||||
|
Go Back to Token Tester
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TokenTesterCallback;
|
||||||
Reference in New Issue
Block a user