<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth 2.1 Test Client</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
max-width: 800px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.section h2 {
color: #333;
font-size: 18px;
margin-bottom: 15px;
}
.info-grid {
display: grid;
grid-template-columns: 150px 1fr;
gap: 10px;
margin-bottom: 15px;
}
.info-label {
font-weight: 600;
color: #555;
}
.info-value {
color: #333;
font-family: 'Courier New', monospace;
font-size: 13px;
word-break: break-all;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
margin-top: 10px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
.code-block {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
margin-top: 10px;
}
.success {
color: #28a745;
font-weight: 600;
}
.error {
color: #dc3545;
font-weight: 600;
}
#result {
margin-top: 20px;
}
.token-display {
background: white;
padding: 15px;
border-radius: 6px;
margin-top: 10px;
border: 1px solid #ddd;
}
.copy-btn {
background: #28a745;
padding: 6px 12px;
font-size: 12px;
margin-left: 10px;
display: inline-block;
width: auto;
}
.step {
margin-bottom: 20px;
}
.step-number {
display: inline-block;
background: #667eea;
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
text-align: center;
line-height: 30px;
font-weight: bold;
margin-right: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>🔐 OAuth 2.1 Test Client</h1>
<p class="subtitle">Interactive OAuth 2.1 Authorization Code Flow with PKCE</p>
<div class="section">
<h2>Configuration</h2>
<div class="info-grid">
<div class="info-label">Authorization Server:</div>
<div class="info-value" id="baseUrl">http://localhost:9000</div>
<div class="info-label">Client ID:</div>
<div class="info-value">public-client</div>
<div class="info-label">Redirect URI:</div>
<div class="info-value">http://localhost:9000/authorized</div>
<div class="info-label">Scopes:</div>
<div class="info-value">profile read write</div>
<div class="info-label">PKCE:</div>
<div class="info-value">✅ Required (S256)</div>
</div>
</div>
<div class="section">
<h2>Step-by-Step Authorization</h2>
<div class="step">
<span class="step-number">1</span>
<strong>Generate PKCE Challenge</strong>
<button onclick="generatePKCE()">Generate PKCE Parameters</button>
<div id="pkceResult"></div>
</div>
<div class="step">
<span class="step-number">2</span>
<strong>Start Authorization</strong>
<button onclick="startAuthorization()" id="authBtn" disabled>Start OAuth 2.1 Flow</button>
<p style="margin-top: 10px; font-size: 13px; color: #666;">
Login credentials: <strong>user/password</strong> or <strong>admin/admin</strong>
</p>
</div>
<div class="step">
<span class="step-number">3</span>
<strong>Authorization Result</strong>
<div id="result"></div>
</div>
</div>
</div>
<script>
let codeVerifier = '';
let codeChallenge = '';
let state = '';
// Generate random string
function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let result = '';
const randomValues = new Uint8Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
result += charset[randomValues[i] % charset.length];
}
return result;
}
// Generate SHA-256 hash
async function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
const hash = await crypto.subtle.digest('SHA-256', data);
return hash;
}
// Base64 URL encode
function base64UrlEncode(arrayBuffer) {
const bytes = new Uint8Array(arrayBuffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Generate PKCE parameters
async function generatePKCE() {
codeVerifier = generateRandomString(128);
state = generateRandomString(32);
const hashed = await sha256(codeVerifier);
codeChallenge = base64UrlEncode(hashed);
document.getElementById('pkceResult').innerHTML = `
<div class="token-display">
<div><strong>Code Verifier:</strong></div>
<div class="code-block">${codeVerifier}</div>
<div style="margin-top: 10px;"><strong>Code Challenge (S256):</strong></div>
<div class="code-block">${codeChallenge}</div>
<div style="margin-top: 10px;"><strong>State:</strong></div>
<div class="code-block">${state}</div>
<p class="success" style="margin-top: 15px;">✓ PKCE parameters generated successfully!</p>
</div>
`;
document.getElementById('authBtn').disabled = false;
// Store in sessionStorage for callback
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('state', state);
}
// Start authorization flow
function startAuthorization() {
const baseUrl = document.getElementById('baseUrl').textContent;
const params = new URLSearchParams({
response_type: 'code',
client_id: 'public-client',
redirect_uri: 'http://localhost:9000/authorized',
scope: 'profile read write',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
const authUrl = `${baseUrl}/oauth2/authorize?${params.toString()}`;
document.getElementById('result').innerHTML = `
<div class="token-display">
<p>Redirecting to authorization server...</p>
<div class="code-block">${authUrl}</div>
</div>
`;
// Redirect to authorization server
window.location.href = authUrl;
}
// Check if we're on the callback page
window.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const returnedState = urlParams.get('state');
const error = urlParams.get('error');
if (error) {
document.getElementById('result').innerHTML = `
<div class="token-display">
<p class="error">❌ Authorization Error</p>
<p><strong>Error:</strong> ${error}</p>
<p><strong>Description:</strong> ${urlParams.get('error_description') || 'No description'}</p>
</div>
`;
return;
}
if (code) {
const storedState = sessionStorage.getItem('state');
const storedVerifier = sessionStorage.getItem('code_verifier');
if (returnedState !== storedState) {
document.getElementById('result').innerHTML = `
<div class="token-display">
<p class="error">❌ State Mismatch - Possible CSRF Attack</p>
</div>
`;
return;
}
document.getElementById('result').innerHTML = `
<div class="token-display">
<p class="success">✓ Authorization Code Received!</p>
<div style="margin-top: 10px;"><strong>Authorization Code:</strong></div>
<div class="code-block">${code}</div>
<button onclick="exchangeToken('${code}', '${storedVerifier}')">Exchange for Access Token</button>
</div>
`;
}
});
// Exchange authorization code for tokens
async function exchangeToken(code, verifier) {
const baseUrl = document.getElementById('baseUrl').textContent;
document.getElementById('result').innerHTML = `
<div class="token-display">
<p>Exchanging authorization code for tokens...</p>
</div>
`;
try {
const response = await fetch(`${baseUrl}/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: 'http://localhost:9000/authorized',
client_id: 'public-client',
code_verifier: verifier
})
});
const data = await response.json();
if (response.ok) {
sessionStorage.setItem('access_token', data.access_token);
document.getElementById('result').innerHTML = `
<div class="token-display">
<p class="success">✓ Tokens Received Successfully!</p>
<div style="margin-top: 15px;"><strong>Access Token:</strong></div>
<div class="code-block">${data.access_token.substring(0, 100)}...</div>
<div style="margin-top: 10px;"><strong>Token Type:</strong> ${data.token_type}</div>
<div style="margin-top: 5px;"><strong>Expires In:</strong> ${data.expires_in} seconds</div>
<div style="margin-top: 5px;"><strong>Scope:</strong> ${data.scope}</div>
${data.refresh_token ? `
<div style="margin-top: 10px;"><strong>Refresh Token:</strong></div>
<div class="code-block">${data.refresh_token.substring(0, 50)}...</div>
` : ''}
<button onclick="getUserInfo()">Get User Info</button>
</div>
`;
} else {
document.getElementById('result').innerHTML = `
<div class="token-display">
<p class="error">❌ Token Exchange Failed</p>
<div class="code-block">${JSON.stringify(data, null, 2)}</div>
</div>
`;
}
} catch (err) {
document.getElementById('result').innerHTML = `
<div class="token-display">
<p class="error">❌ Error: ${err.message}</p>
</div>
`;
}
}
// Get user info
async function getUserInfo() {
const baseUrl = document.getElementById('baseUrl').textContent;
const accessToken = sessionStorage.getItem('access_token');
try {
const response = await fetch(`${baseUrl}/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const data = await response.json();
if (response.ok) {
document.getElementById('result').innerHTML += `
<div class="token-display" style="margin-top: 15px;">
<p class="success">✓ User Info Retrieved!</p>
<div class="code-block">${JSON.stringify(data, null, 2)}</div>
</div>
`;
} else {
document.getElementById('result').innerHTML += `
<div class="token-display" style="margin-top: 15px;">
<p class="error">❌ Failed to get user info</p>
<div class="code-block">${JSON.stringify(data, null, 2)}</div>
</div>
`;
}
} catch (err) {
document.getElementById('result').innerHTML += `
<div class="token-display" style="margin-top: 15px;">
<p class="error">❌ Error: ${err.message}</p>
</div>
`;
}
}
</script>
</body>
</html>