authorization/authorization_code_pkce/public/app.js (136 lines of code) (raw):

/** * This is an example of a basic node.js script that performs * the Authorization Code with PKCE oAuth2 flow to authenticate * against the Spotify Accounts. * * For more information, read * https://developer.spotify.com/documentation/web-api/tutorials/code-pkce-flow */ const clientId = 'yourClientIDGoesHere'; // your clientId const redirectUrl = 'eg:http://localhost:8080'; // your redirect URL - must be localhost URL and/or HTTPS const authorizationEndpoint = "https://accounts.spotify.com/authorize"; const tokenEndpoint = "https://accounts.spotify.com/api/token"; const scope = 'user-read-private user-read-email'; // Data structure that manages the current active token, caching it in localStorage const currentToken = { get access_token() { return localStorage.getItem('access_token') || null; }, get refresh_token() { return localStorage.getItem('refresh_token') || null; }, get expires_in() { return localStorage.getItem('refresh_in') || null }, get expires() { return localStorage.getItem('expires') || null }, save: function (response) { const { access_token, refresh_token, expires_in } = response; localStorage.setItem('access_token', access_token); localStorage.setItem('refresh_token', refresh_token); localStorage.setItem('expires_in', expires_in); const now = new Date(); const expiry = new Date(now.getTime() + (expires_in * 1000)); localStorage.setItem('expires', expiry); } }; // On page load, try to fetch auth code from current browser search URL const args = new URLSearchParams(window.location.search); const code = args.get('code'); // If we find a code, we're in a callback, do a token exchange if (code) { const token = await getToken(code); currentToken.save(token); // Remove code from URL so we can refresh correctly. const url = new URL(window.location.href); url.searchParams.delete("code"); const updatedUrl = url.search ? url.href : url.href.replace('?', ''); window.history.replaceState({}, document.title, updatedUrl); } // If we have a token, we're logged in, so fetch user data and render logged in template if (currentToken.access_token) { const userData = await getUserData(); renderTemplate("main", "logged-in-template", userData); renderTemplate("oauth", "oauth-template", currentToken); } // Otherwise we're not logged in, so render the login template if (!currentToken.access_token) { renderTemplate("main", "login"); } async function redirectToSpotifyAuthorize() { const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const randomValues = crypto.getRandomValues(new Uint8Array(64)); const randomString = randomValues.reduce((acc, x) => acc + possible[x % possible.length], ""); const code_verifier = randomString; const data = new TextEncoder().encode(code_verifier); const hashed = await crypto.subtle.digest('SHA-256', data); const code_challenge_base64 = btoa(String.fromCharCode(...new Uint8Array(hashed))) .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); window.localStorage.setItem('code_verifier', code_verifier); const authUrl = new URL(authorizationEndpoint) const params = { response_type: 'code', client_id: clientId, scope: scope, code_challenge_method: 'S256', code_challenge: code_challenge_base64, redirect_uri: redirectUrl, }; authUrl.search = new URLSearchParams(params).toString(); window.location.href = authUrl.toString(); // Redirect the user to the authorization server for login } // Soptify API Calls async function getToken(code) { const code_verifier = localStorage.getItem('code_verifier'); const response = await fetch(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ client_id: clientId, grant_type: 'authorization_code', code: code, redirect_uri: redirectUrl, code_verifier: code_verifier, }), }); return await response.json(); } async function refreshToken() { const response = await fetch(tokenEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ client_id: clientId, grant_type: 'refresh_token', refresh_token: currentToken.refresh_token }), }); return await response.json(); } async function getUserData() { const response = await fetch("https://api.spotify.com/v1/me", { method: 'GET', headers: { 'Authorization': 'Bearer ' + currentToken.access_token }, }); return await response.json(); } // Click handlers async function loginWithSpotifyClick() { await redirectToSpotifyAuthorize(); } async function logoutClick() { localStorage.clear(); window.location.href = redirectUrl; } async function refreshTokenClick() { const token = await refreshToken(); currentToken.save(token); renderTemplate("oauth", "oauth-template", currentToken); } // HTML Template Rendering with basic data binding - demoware only. function renderTemplate(targetId, templateId, data = null) { const template = document.getElementById(templateId); const clone = template.content.cloneNode(true); const elements = clone.querySelectorAll("*"); elements.forEach(ele => { const bindingAttrs = [...ele.attributes].filter(a => a.name.startsWith("data-bind")); bindingAttrs.forEach(attr => { const target = attr.name.replace(/data-bind-/, "").replace(/data-bind/, ""); const targetType = target.startsWith("onclick") ? "HANDLER" : "PROPERTY"; const targetProp = target === "" ? "innerHTML" : target; const prefix = targetType === "PROPERTY" ? "data." : ""; const expression = prefix + attr.value.replace(/;\n\r\n/g, ""); // Maybe use a framework with more validation here ;) try { ele[targetProp] = targetType === "PROPERTY" ? eval(expression) : () => { eval(expression) }; ele.removeAttribute(attr.name); } catch (ex) { console.error(`Error binding ${expression} to ${targetProp}`, ex); } }); }); const target = document.getElementById(targetId); target.innerHTML = ""; target.appendChild(clone); }