PKCE
Proof Key for Code Exchange): For mobile or Single Page Applications (SPAs) we shoulduse Authorization Code Grant with PKCE.
Keycloak PKCE client Gemini generated
- Could you implement a javascript client with html with no nodejs need ?
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <title>OAuth2 PKCE Client</title>
5 <style>
6 body { font-family: sans-serif; padding: 20px; line-height: 1.6; }
7 .container { max-width: 600px; margin: auto; border: 1px solid #ccc; padding: 20px; border-radius: 8px; }
8 pre { background: #f4f4f4; padding: 10px; overflow-x: auto; font-size: 12px; }
9 button { padding: 10px 20px; cursor: pointer; background: #0078d4; color: white; border: none; border-radius: 4px; }
10 </style>
11 </head>
12 <body>
13
14 <div class="container">
15 <h2>OAuth2 JS Client (No Node.js)</h2>
16 <div id="auth-status">Checking status...</div>
17 <br>
18 <button id="login-btn" style="display:none;">Log In</button>
19 <button id="logout-btn" style="display:none;">Log Out</button>
20
21 <h3>Token Info:</h3>
22 <pre id="token-data">Not logged in.</pre>
23 </div>
24
25 <script>
26 // --- CONFIGURATION ---
27 const config = {
28 clientId: "YOUR_CLIENT_ID",
29 authEndpoint: "https://{KEYCLOAK_OR_ENTRA_URL}/authorize",
30 tokenEndpoint: "https://{KEYCLOAK_OR_ENTRA_URL}/token",
31 redirectUri: window.location.origin + window.location.pathname,
32 scope: "openid profile"
33 };
34
35 // --- PKCE HELPERS ---
36 async function generateCodeChallenge(codeVerifier) {
37 const encoder = new TextEncoder();
38 const data = encoder.encode(codeVerifier);
39 const digest = await window.crypto.subtle.digest("SHA-256", data);
40 return btoa(String.fromCharCode(...new Uint8Array(digest)))
41 .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
42 }
43
44 function generateRandomString(length) {
45 const array = new Uint8Array(length);
46 window.crypto.getRandomValues(array);
47 return btoa(String.fromCharCode(...array))
48 .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "").substring(0, length);
49 }
50
51 // --- MAIN LOGIC ---
52 async function init() {
53 const params = new URLSearchParams(window.location.search);
54 const code = params.get("code");
55
56 if (code) {
57 // 1. Exchange code for token
58 await exchangeCodeForToken(code);
59 window.history.replaceState({}, document.title, window.location.pathname); // Clean URL
60 } else if (localStorage.getItem("access_token")) {
61 // 2. Already logged in
62 showLoggedIn();
63 } else {
64 // 3. Show login button
65 document.getElementById("login-btn").style.display = "block";
66 document.getElementById("auth-status").innerText = "Please log in.";
67 }
68 }
69
70 document.getElementById("login-btn").onclick = async () => {
71 const verifier = generateRandomString(64);
72 localStorage.setItem("code_verifier", verifier);
73 const challenge = await generateCodeChallenge(verifier);
74
75 const authUrl = `${config.authEndpoint}?` +
76 `client_id=${config.clientId}&` +
77 `response_type=code&` +
78 `redirect_uri=${encodeURIComponent(config.redirectUri)}&` +
79 `scope=${encodeURIComponent(config.scope)}&` +
80 `code_challenge=${challenge}&` +
81 `code_challenge_method=S256`;
82
83 window.location.href = authUrl;
84 };
85
86 async function exchangeCodeForToken(code) {
87 const verifier = localStorage.getItem("code_verifier");
88
89 const response = await fetch(config.tokenEndpoint, {
90 method: "POST",
91 headers: { "Content-Type": "application/x-www-form-urlencoded" },
92 body: new URLSearchParams({
93 grant_type: "authorization_code",
94 client_id: config.clientId,
95 code: code,
96 redirect_uri: config.redirectUri,
97 code_verifier: verifier
98 })
99 });
100
101 const data = await response.json();
102 if (data.access_token) {
103 localStorage.setItem("access_token", data.access_token);
104 showLoggedIn();
105 }
106 }
107
108 function showLoggedIn() {
109 document.getElementById("login-btn").style.display = "none";
110 document.getElementById("logout-btn").style.display = "block";
111 document.getElementById("auth-status").innerText = "Status: Logged In";
112 document.getElementById("token-data").innerText = localStorage.getItem("access_token");
113 }
114
115 document.getElementById("logout-btn").onclick = () => {
116 localStorage.clear();
117 location.reload();
118 };
119
120 init();
121 </script>
122 </body>
123 </html>
Azure Entra ID PKCE client - Gemini generated
- Can you adapt the html to azure entra id endpoints ?
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <title>Azure Entra ID PKCE Client</title>
5 <style>
6 body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 40px; background-color: #f3f2f1; }
7 .container { max-width: 800px; margin: auto; background: white; padding: 30px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
8 pre { background: #201f1e; color: #00eb00; padding: 15px; overflow-x: auto; border-radius: 4px; font-family: 'Cascadia Code', monospace; }
9 button { padding: 12px 24px; cursor: pointer; background: #0078d4; color: white; border: none; font-weight: 600; }
10 button:hover { background: #005a9e; }
11 .hidden { display: none; }
12 </style>
13 </head>
14 <body>
15
16 <div class="container">
17 <h2>Azure Entra ID Auth (Plain JS)</h2>
18 <p id="auth-status">Checking session...</p>
19
20 <button id="login-btn" class="hidden">Sign in with Microsoft</button>
21 <button id="logout-btn" class="hidden">Sign Out</button>
22
23 <h3>Token Response:</h3>
24 <pre id="token-data">Not authenticated.</pre>
25 </div>
26
27 <script>
28 // --- 1. CONFIGURE THESE THREE VALUES ---
29 const TENANT_ID = "YOUR_TENANT_ID"; // Or "common" for multi-tenant
30 const CLIENT_ID = "YOUR_APPLICATION_CLIENT_ID";
31 const REDIRECT_URI = window.location.origin + window.location.pathname;
32
33 const config = {
34 authEndpoint: `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/authorize`,
35 tokenEndpoint: `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`,
36 // scope: "openid profile offline_access User.Read"
37 scope: "openid profile User.Read"
38 };
39
40 // --- PKCE CRYPTO UTILS ---
41 async function generateCodeChallenge(codeVerifier) {
42 const encoder = new TextEncoder();
43 const data = encoder.encode(codeVerifier);
44 const digest = await window.crypto.subtle.digest("SHA-256", data);
45 return btoa(String.fromCharCode(...new Uint8Array(digest)))
46 .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
47 }
48
49 function generateRandomString(length) {
50 const array = new Uint8Array(length);
51 window.crypto.getRandomValues(array);
52 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
53 let result = '';
54 for (let i = 0; i < length; i++) { result += chars.charAt(array[i] % chars.length); }
55 return result;
56 }
57
58 // --- MAIN FLOW ---
59 async function handleAuth() {
60 const params = new URLSearchParams(window.location.search);
61 const code = params.get("code");
62
63 if (code) {
64 await exchangeCodeForToken(code);
65 window.history.replaceState({}, document.title, window.location.pathname);
66 } else if (localStorage.getItem("azure_access_token")) {
67 showLoggedIn();
68 } else {
69 document.getElementById("login-btn").classList.remove("hidden");
70 }
71 }
72
73 document.getElementById("login-btn").onclick = async () => {
74 const verifier = generateRandomString(64);
75 localStorage.setItem("pkce_verifier", verifier);
76 const challenge = await generateCodeChallenge(verifier);
77
78 const authUrl = `${config.authEndpoint}?` +
79 `client_id=${CLIENT_ID}&` +
80 `response_type=code&` +
81 `redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
82 `response_mode=query&` +
83 `scope=${encodeURIComponent(config.scope)}&` +
84 `code_challenge=${challenge}&` +
85 `code_challenge_method=S256`;
86
87 window.location.href = authUrl;
88 };
89
90 async function exchangeCodeForToken(code) {
91 const verifier = localStorage.getItem("pkce_verifier");
92
93 const response = await fetch(config.tokenEndpoint, {
94 method: "POST",
95 headers: { "Content-Type": "application/x-www-form-urlencoded" },
96 body: new URLSearchParams({
97 client_id: CLIENT_ID,
98 grant_type: "authorization_code",
99 code: code,
100 redirect_uri: REDIRECT_URI,
101 code_verifier: verifier
102 // No client_secret needed for SPA/Public clients
103 })
104 });
105
106 const data = await response.json();
107 if (data.access_token) {
108 localStorage.setItem("azure_access_token", data.access_token);
109 localStorage.setItem("full_response", JSON.stringify(data, null, 2));
110 showLoggedIn();
111 } else {
112 alert("Error: " + data.error_description);
113 }
114 }
115
116 function showLoggedIn() {
117 document.getElementById("login-btn").classList.add("hidden");
118 document.getElementById("logout-btn").classList.remove("hidden");
119 document.getElementById("auth-status").innerText = "Status: Signed in to Azure Entra ID";
120 document.getElementById("token-data").innerText = localStorage.getItem("full_response");
121 }
122
123 document.getElementById("logout-btn").onclick = () => {
124 localStorage.clear();
125 window.location.reload();
126 };
127
128 handleAuth();
129 </script>
130 </body>
131 </html>