name 9f2f9f470f Add AWS Operations Conversational Agent use case
- Complete serverless AI-powered AWS operations platform
- Multi-Lambda architecture with Function URL deployment
- Bedrock AgentCore Gateway integration with MCP protocol
- 20 AWS service tools for comprehensive operations
- Dual authentication: AWS SigV4 + Okta JWT
- Natural language interface with streaming responses
- DynamoDB conversation persistence
- Docker-based MCP Tool Lambda with Strands framework
- Production-ready with enterprise security patterns
- Comprehensive documentation and setup guides
- Read-only operations by default with write enablement guide
- Interactive client with CLI interface
- Complete Okta OAuth2 PKCE setup
- Management scripts for gateway and target operations
- Sanitized configuration with dummy data for public sharing
2025-07-15 17:30:49 -07:00

887 lines
35 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PKCE OpenID Flow - No Redirects</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.step {
margin: 20px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
}
.step.active {
border-color: #007bff;
background: #f0f8ff;
}
.step.completed {
border-color: #28a745;
background: #f0fff0;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #555;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
button {
background: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin: 8px 4px;
transition: background-color 0.2s;
}
button:hover { background: #0056b3; }
button:disabled {
background: #6c757d;
cursor: not-allowed;
}
button.success {
background: #28a745;
}
button.success:hover {
background: #218838;
}
.token-display {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 12px;
word-break: break-all;
margin: 10px 0;
border: 1px solid #dee2e6;
max-height: 200px;
overflow-y: auto;
}
.status {
margin: 15px 0;
padding: 12px;
border-radius: 6px;
font-weight: 500;
}
.status.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
.status.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
.status.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden { display: none; }
.iframe-container {
border: 2px solid #007bff;
border-radius: 8px;
margin: 20px 0;
background: #f8f9fa;
padding: 10px;
}
.oauth-iframe {
width: 100%;
height: 400px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
}
.final-result {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 20px;
margin-top: 20px;
}
.progress {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}
.progress-step {
flex: 1;
text-align: center;
padding: 10px;
background: #f8f9fa;
border: 1px solid #dee2e6;
margin: 0 2px;
border-radius: 4px;
font-size: 12px;
}
.progress-step.active {
background: #007bff;
color: white;
}
.progress-step.completed {
background: #28a745;
color: white;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 PKCE OpenID Flow</h1>
<p>Complete OpenID Connect authentication with PKCE using iframe (no page redirects)</p>
</div>
<div class="progress">
<div class="progress-step active" id="step0-progress">0. Config</div>
<div class="progress-step" id="step1-progress">1. Login</div>
<div class="progress-step" id="step2-progress">2. Session Token</div>
<div class="progress-step" id="step3-progress">3. iframe PKCE</div>
<div class="progress-step" id="step4-progress">4. OAuth Tokens</div>
</div>
<!-- Step 0: Configuration -->
<div class="step active" id="step0">
<h3>⚙️ Step 0: Okta Configuration</h3>
<p>Configure your Okta environment settings:</p>
<div class="form-group">
<label for="oktaDomain">Okta Domain:</label>
<input type="text" id="oktaDomain" name="oktaDomain" value="dev-09210948.okta.com" required>
<small style="color: #666; font-size: 12px;">Example: dev-12345678.okta.com or your-company.okta.com</small>
</div>
<div class="form-group">
<label for="clientId">Client ID:</label>
<input type="text" id="clientId" name="clientId" value="0oapcjro7liptyDSN5d7" required>
<small style="color: #666; font-size: 12px;">Your Okta application's client ID</small>
</div>
<div class="form-group">
<label for="redirectUri">Redirect URI:</label>
<input type="text" id="redirectUri" name="redirectUri" value="http://localhost/okta-auth/iframe-callback.html" required>
<small style="color: #666; font-size: 12px;">Must match your Okta app configuration</small>
</div>
<div class="form-group">
<label for="authServerId">Authorization Server ID:</label>
<input type="text" id="authServerId" name="authServerId" value="default" required>
<small style="color: #666; font-size: 12px;">Usually 'default' or custom authorization server ID</small>
</div>
<div class="form-group">
<h4 style="margin-top: 25px; margin-bottom: 15px; color: #555;">📍 Generated Endpoints:</h4>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border: 1px solid #dee2e6;">
<div style="margin-bottom: 10px;">
<strong>Base URL:</strong><br>
<code id="displayBaseUrl">https://dev-09210948.okta.com</code>
</div>
<div style="margin-bottom: 10px;">
<strong>Authorization Endpoint:</strong><br>
<code id="displayAuthEndpoint">https://dev-09210948.okta.com/oauth2/default/v1/authorize</code>
</div>
<div style="margin-bottom: 10px;">
<strong>Token Endpoint:</strong><br>
<code id="displayTokenEndpoint">https://dev-09210948.okta.com/oauth2/default/v1/token</code>
</div>
<div>
<strong>Authentication API:</strong><br>
<code id="displayAuthApi">https://dev-09210948.okta.com/api/v1/authn</code>
</div>
</div>
</div>
<button onclick="validateAndSaveConfig()">
<span id="configSpinner" class="spinner hidden"></span>
<span id="configText">✅ Validate & Save Configuration</span>
</button>
<div style="margin-top: 15px;">
<button onclick="loadPresetConfig('demo')" style="background: #6c757d;">
📋 Load Demo Config
</button>
<button onclick="loadPresetConfig('production')" style="background: #6c757d;">
🏢 Load Production Template
</button>
</div>
</div>
<!-- Step 1: Login -->
<div class="step hidden" id="step1">
<h3>🔐 Step 1: Okta Login</h3>
<div class="form-group">
<label for="username">Username or Email:</label>
<input type="text" id="username" name="username" placeholder="Enter your username or email" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" placeholder="Enter your password" required>
</div>
<button onclick="performLogin()">
<span id="loginSpinner" class="spinner hidden"></span>
<span id="loginText">🚀 Login to Okta</span>
</button>
</div>
<!-- Step 2: Session Token Display -->
<div class="step hidden" id="step2">
<h3>🎫 Step 2: Session Token Retrieved</h3>
<p>Successfully authenticated with Okta!</p>
<div class="token-display" id="sessionTokenDisplay"></div>
<button onclick="startIframePKCE()" class="success">
✅ Continue to iframe PKCE Flow
</button>
</div>
<!-- Step 3: iframe PKCE Flow -->
<div class="step hidden" id="step3">
<h3>🖼️ Step 3: iframe PKCE Authorization</h3>
<p>Using iframe to capture authorization code without page redirect:</p>
<div id="iframeStatus" class="status info">
<span class="spinner"></span>
<span>Preparing iframe PKCE flow...</span>
</div>
<div class="iframe-container hidden" id="iframeContainer">
<p><strong>OAuth Authorization in iframe:</strong></p>
<iframe id="oauthIframe" class="oauth-iframe" src="about:blank"></iframe>
<div class="status info">
<span>🔍 Monitoring iframe for authorization code...</span>
</div>
</div>
</div>
<!-- Step 4: Final Results -->
<div class="step hidden" id="finalResults">
<h3>🎉 Success! Your OAuth Tokens</h3>
<h4>Access Token:</h4>
<div class="token-display" id="finalAccessToken"></div>
<button onclick="copyToken('access')">📋 Copy Access Token</button>
<button onclick="decodeToken('access')">🔍 Decode Access Token</button>
<h4>ID Token:</h4>
<div class="token-display" id="finalIdToken"></div>
<button onclick="copyToken('id')">📋 Copy ID Token</button>
<button onclick="decodeToken('id')">🔍 Decode ID Token</button>
<div class="final-result">
<h4>🌐 Ready for API Integration!</h4>
<p><strong>Use this Authorization header:</strong></p>
<div class="token-display">Authorization: Bearer <span id="tokenForHeader"></span></div>
<button onclick="copyAuthHeader()">📋 Copy Authorization Header</button>
</div>
</div>
<div id="decodedTokens" class="step hidden">
<h3>🔍 Decoded Token Information</h3>
<div class="token-display" id="decodedDisplay"></div>
</div>
<div id="globalStatus" class="status info hidden">
<span id="globalStatusText"></span>
</div>
</div>
<script>
// Okta Configuration (will be populated from user input)
let oktaConfig = {
domain: '',
clientId: '',
redirectUri: '',
baseUrl: '',
authServerId: '',
authEndpoint: '',
tokenEndpoint: '',
authApiEndpoint: ''
};
// Global state
let currentStep = 0;
let sessionToken = null;
let pkceParams = {};
let tokens = { accessToken: null, idToken: null };
let iframeMonitorInterval = null;
// Step 0: Configuration Management
function updateEndpointDisplays() {
const domain = document.getElementById('oktaDomain').value;
const authServerId = document.getElementById('authServerId').value;
const baseUrl = `https://${domain}`;
const authEndpoint = `${baseUrl}/oauth2/${authServerId}/v1/authorize`;
const tokenEndpoint = `${baseUrl}/oauth2/${authServerId}/v1/token`;
const authApiEndpoint = `${baseUrl}/api/v1/authn`;
document.getElementById('displayBaseUrl').textContent = baseUrl;
document.getElementById('displayAuthEndpoint').textContent = authEndpoint;
document.getElementById('displayTokenEndpoint').textContent = tokenEndpoint;
document.getElementById('displayAuthApi').textContent = authApiEndpoint;
}
function loadPresetConfig(preset) {
if (preset === 'demo') {
document.getElementById('oktaDomain').value = 'dev-09210948.okta.com';
document.getElementById('clientId').value = '0oapcjro7liptyDSN5d7';
document.getElementById('redirectUri').value = 'http://localhost/okta-auth/iframe-callback.html';
document.getElementById('authServerId').value = 'default';
} else if (preset === 'production') {
document.getElementById('oktaDomain').value = 'your-company.okta.com';
document.getElementById('clientId').value = 'your-client-id-here';
document.getElementById('redirectUri').value = 'https://your-app.com/callback';
document.getElementById('authServerId').value = 'default';
}
updateEndpointDisplays();
showGlobalStatus(`${preset} configuration template loaded`, 'success');
}
async function validateAndSaveConfig() {
const domain = document.getElementById('oktaDomain').value.trim();
const clientId = document.getElementById('clientId').value.trim();
const redirectUri = document.getElementById('redirectUri').value.trim();
const authServerId = document.getElementById('authServerId').value.trim();
// Validation
if (!domain || !clientId || !redirectUri || !authServerId) {
showGlobalStatus('Please fill in all configuration fields', 'error');
return;
}
// Validate domain format
if (!domain.includes('.') || domain.startsWith('http')) {
showGlobalStatus('Domain should be just the hostname (e.g., dev-12345678.okta.com)', 'error');
return;
}
// Validate redirect URI format
try {
new URL(redirectUri);
} catch (e) {
showGlobalStatus('Redirect URI must be a valid URL', 'error');
return;
}
setConfigLoading(true);
showGlobalStatus('Validating Okta configuration...', 'info');
try {
// Build configuration
oktaConfig = {
domain: domain,
clientId: clientId,
redirectUri: redirectUri,
baseUrl: `https://${domain}`,
authServerId: authServerId,
authEndpoint: `https://${domain}/oauth2/${authServerId}/v1/authorize`,
tokenEndpoint: `https://${domain}/oauth2/${authServerId}/v1/token`,
authApiEndpoint: `https://${domain}/api/v1/authn`
};
// Test connectivity to Okta (optional - just check if domain resolves)
const testResponse = await fetch(`${oktaConfig.baseUrl}/.well-known/openid_configuration`, {
method: 'GET',
mode: 'cors'
}).catch(() => null);
if (testResponse && testResponse.ok) {
const wellKnown = await testResponse.json();
showGlobalStatus('✅ Okta configuration validated successfully!', 'success');
// Optionally verify endpoints match
if (wellKnown.authorization_endpoint !== oktaConfig.authEndpoint) {
showGlobalStatus(`⚠️ Warning: Authorization endpoint mismatch. Expected: ${wellKnown.authorization_endpoint}`, 'info');
}
} else {
showGlobalStatus('⚠️ Could not validate Okta endpoints, but configuration saved', 'info');
}
// Move to next step
updateProgress(1);
showStep(1);
showGlobalStatus('Configuration saved! Ready to login.', 'success');
} catch (error) {
showGlobalStatus(`Configuration validation error: ${error.message}`, 'error');
} finally {
setConfigLoading(false);
}
}
function setConfigLoading(loading) {
const spinner = document.getElementById('configSpinner');
const text = document.getElementById('configText');
const button = document.querySelector('#step0 button');
if (loading) {
spinner.classList.remove('hidden');
text.textContent = 'Validating...';
button.disabled = true;
} else {
spinner.classList.add('hidden');
text.textContent = '✅ Validate & Save Configuration';
button.disabled = false;
}
}
// Add event listeners for real-time endpoint updates
document.addEventListener('DOMContentLoaded', function() {
const domainInput = document.getElementById('oktaDomain');
const authServerInput = document.getElementById('authServerId');
domainInput.addEventListener('input', updateEndpointDisplays);
authServerInput.addEventListener('input', updateEndpointDisplays);
// Initialize displays
updateEndpointDisplays();
});
// Step 1: Perform Login
async function performLogin() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!username || !password) {
showGlobalStatus('Please enter both username and password', 'error');
return;
}
setLoginLoading(true);
showGlobalStatus('Authenticating with Okta...', 'info');
try {
const authUrl = oktaConfig.authApiEndpoint;
const response = await fetch(authUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
username: username,
password: password,
options: {
multiOptionalFactorEnroll: false,
warnBeforePasswordExpired: false
}
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.errorSummary || `HTTP ${response.status}`);
}
const data = await response.json();
if (data.status === 'SUCCESS') {
sessionToken = data.sessionToken;
showStep2Success();
} else {
throw new Error(`Authentication failed: ${data.status}`);
}
} catch (error) {
showGlobalStatus(`Login failed: ${error.message}`, 'error');
} finally {
setLoginLoading(false);
}
}
// Step 2: Show session token success
function showStep2Success() {
updateProgress(2);
document.getElementById('sessionTokenDisplay').textContent = sessionToken;
showStep(2);
showGlobalStatus('✅ Login successful! Session token retrieved.', 'success');
}
// Step 3: Start iframe PKCE Flow
async function startIframePKCE() {
updateProgress(3);
showStep(3);
showGlobalStatus('Starting iframe PKCE authorization flow...', 'info');
try {
// Generate PKCE parameters
pkceParams.codeVerifier = generateCodeVerifier();
pkceParams.codeChallenge = await generateCodeChallenge(pkceParams.codeVerifier);
pkceParams.state = generateRandomString();
pkceParams.nonce = generateRandomString();
// Build authorization URL
const authUrl = new URL(oktaConfig.authEndpoint);
const params = {
'client_id': oktaConfig.clientId,
'response_type': 'code',
'scope': 'openid profile email',
'redirect_uri': oktaConfig.redirectUri,
'state': pkceParams.state,
'nonce': pkceParams.nonce,
'code_challenge': pkceParams.codeChallenge,
'code_challenge_method': 'S256',
'sessionToken': sessionToken
};
Object.keys(params).forEach(key => {
authUrl.searchParams.append(key, params[key]);
});
// Show iframe container
document.getElementById('iframeContainer').classList.remove('hidden');
// Load authorization URL in iframe
const iframe = document.getElementById('oauthIframe');
iframe.src = authUrl.toString();
// Update status
document.getElementById('iframeStatus').innerHTML = `
<span class="spinner"></span>
<span>Loading OAuth authorization in iframe...</span>
`;
// Start monitoring iframe for callback
startIframeMonitoring();
} catch (error) {
showGlobalStatus(`iframe PKCE flow error: ${error.message}`, 'error');
}
}
// Monitor iframe for OAuth callback
function startIframeMonitoring() {
const iframe = document.getElementById('oauthIframe');
let attempts = 0;
const maxAttempts = 60; // 60 seconds timeout
iframeMonitorInterval = setInterval(() => {
attempts++;
try {
// Try to access iframe URL
const iframeUrl = iframe.contentWindow.location.href;
// Check if we got redirected to callback with code
if (iframeUrl.includes('code=')) {
clearInterval(iframeMonitorInterval);
const urlParams = new URLSearchParams(iframeUrl.split('?')[1]);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code && state === pkceParams.state) {
document.getElementById('iframeStatus').innerHTML = `
<div class="status success">✅ Authorization code captured from iframe!</div>
`;
exchangeTokens(code, pkceParams.codeVerifier);
} else {
throw new Error('Invalid authorization response');
}
}
} catch (e) {
// Cross-origin error is expected while iframe is on Okta domain
// We'll continue monitoring
}
// Timeout after maxAttempts
if (attempts >= maxAttempts) {
clearInterval(iframeMonitorInterval);
document.getElementById('iframeStatus').innerHTML = `
<div class="status error">⏰ Timeout waiting for authorization. Please try again.</div>
`;
}
// Update status every 5 seconds
if (attempts % 5 === 0) {
document.getElementById('iframeStatus').innerHTML = `
<span class="spinner"></span>
<span>Waiting for OAuth authorization... (${attempts}s)</span>
`;
}
}, 1000);
}
// Exchange tokens
async function exchangeTokens(code, codeVerifier) {
try {
updateProgress(4);
showGlobalStatus('Exchanging authorization code for tokens...', 'info');
const tokenResponse = await fetch(oktaConfig.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: new URLSearchParams({
'grant_type': 'authorization_code',
'client_id': oktaConfig.clientId,
'code': code,
'redirect_uri': oktaConfig.redirectUri,
'code_verifier': codeVerifier
})
});
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
throw new Error(`Token exchange failed: ${tokenResponse.status} - ${errorText}`);
}
const tokenData = await tokenResponse.json();
tokens.accessToken = tokenData.access_token;
tokens.idToken = tokenData.id_token;
showFinalResults();
} catch (error) {
showGlobalStatus(`Token exchange error: ${error.message}`, 'error');
}
}
// Show final results
function showFinalResults() {
updateProgress(4, true);
showStep('finalResults');
document.getElementById('finalAccessToken').textContent = tokens.accessToken;
document.getElementById('finalIdToken').textContent = tokens.idToken;
document.getElementById('tokenForHeader').textContent = tokens.accessToken;
showGlobalStatus('🎉 PKCE OpenID flow completed successfully!', 'success');
}
// Utility functions
function updateProgress(step, completed = false) {
for (let i = 0; i <= 4; i++) {
const elem = document.getElementById(`step${i}-progress`);
if (elem) {
elem.classList.remove('active', 'completed');
if (i < step || (i === step && completed)) {
elem.classList.add('completed');
} else if (i === step) {
elem.classList.add('active');
}
}
}
currentStep = step;
}
function showStep(stepId) {
document.querySelectorAll('.step').forEach(step => {
step.classList.remove('active');
step.classList.add('hidden');
});
const targetStep = typeof stepId === 'string' ?
document.getElementById(stepId) :
document.getElementById(`step${stepId}`);
if (targetStep) {
targetStep.classList.remove('hidden');
targetStep.classList.add('active');
}
}
function setLoginLoading(loading) {
const spinner = document.getElementById('loginSpinner');
const text = document.getElementById('loginText');
const button = document.querySelector('#step1 button');
if (loading) {
spinner.classList.remove('hidden');
text.textContent = 'Authenticating...';
button.disabled = true;
} else {
spinner.classList.add('hidden');
text.textContent = '🚀 Login to Okta';
button.disabled = false;
}
}
function showGlobalStatus(message, type) {
const statusDiv = document.getElementById('globalStatus');
const statusText = document.getElementById('globalStatusText');
statusText.textContent = message;
statusDiv.className = `status ${type}`;
statusDiv.classList.remove('hidden');
if (type === 'success') {
setTimeout(() => {
statusDiv.classList.add('hidden');
}, 5000);
}
}
function copyToken(type) {
const token = type === 'access' ? tokens.accessToken : tokens.idToken;
if (!token) return;
navigator.clipboard.writeText(token).then(() => {
showGlobalStatus(`${type} token copied to clipboard!`, 'success');
});
}
function copyAuthHeader() {
const header = `Authorization: Bearer ${tokens.accessToken}`;
navigator.clipboard.writeText(header).then(() => {
showGlobalStatus('Authorization header copied to clipboard!', 'success');
});
}
function decodeToken(type) {
console.log('decodeToken called with type:', type);
console.log('Current tokens:', tokens);
const token = type === 'access' ? tokens.accessToken : tokens.idToken;
if (!token) {
showGlobalStatus(`No ${type} token available to decode`, 'error');
return;
}
try {
console.log('Attempting to decode token:', token.substring(0, 50) + '...');
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format - token must have 3 parts');
}
// Decode header and payload
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
console.log('Decoded header:', header);
console.log('Decoded payload:', payload);
const decoded = {
tokenType: type.toUpperCase() + ' TOKEN',
header: header,
payload: payload,
claims: {
issuer: payload.iss,
audience: payload.aud,
subject: payload.sub,
clientId: payload.cid,
scopes: payload.scp,
issued: new Date(payload.iat * 1000).toLocaleString(),
expires: new Date(payload.exp * 1000).toLocaleString(),
authTime: payload.auth_time ? new Date(payload.auth_time * 1000).toLocaleString() : 'N/A'
}
};
// Add ID token specific claims
if (type === 'id') {
decoded.claims.name = payload.name || 'N/A';
decoded.claims.email = payload.email || 'N/A';
decoded.claims.preferredUsername = payload.preferred_username || 'N/A';
}
document.getElementById('decodedDisplay').textContent = JSON.stringify(decoded, null, 2);
document.getElementById('decodedTokens').classList.remove('hidden');
showGlobalStatus(`${type.toUpperCase()} token decoded successfully!`, 'success');
} catch (error) {
console.error('Token decode error:', error);
showGlobalStatus(`Error decoding ${type} token: ${error.message}`, 'error');
// Show raw token for debugging
document.getElementById('decodedDisplay').textContent = `
ERROR DECODING TOKEN:
${error.message}
RAW TOKEN:
${token}
TOKEN PARTS:
${token.split('.').map((part, i) => `Part ${i + 1}: ${part}`).join('\n')}
`;
document.getElementById('decodedTokens').classList.remove('hidden');
}
}
// PKCE utility functions
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64URLEncode(new Uint8Array(digest));
}
function base64URLEncode(array) {
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
function generateRandomString() {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return base64URLEncode(array);
}
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (iframeMonitorInterval) {
clearInterval(iframeMonitorInterval);
}
});
</script>
</body>
</html>