Newer
Older
springboot-auth201 / src / main / resources / static / test-client.html
@agalyaramadoss agalyaramadoss on 29 Nov 16 KB added client test page
<!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>