diff --git a/get_user_profile/readme.md b/get_user_profile/readme.md index 4a0ba540..6afe638f 100644 --- a/get_user_profile/readme.md +++ b/get_user_profile/readme.md @@ -12,9 +12,13 @@ To run this demo you will need: ## Usage -Clone the repository, cd into the `get_user_profile` directory and run: +Create an app in your [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/), set the redirect URI to ` http://localhost:5173/callback` and `http://localhost:5173/callback/` and copy your Client ID. + +Clone the repository, ensure that you are in the `get_user_profile` directory and run: ```bash npm install npm run dev ``` + +Replace the value for clientId in `/src/script.ts` with your own Client ID. diff --git a/get_user_profile/src/authCodeWithPkce.ts b/get_user_profile/src/authCodeWithPkce.ts new file mode 100644 index 00000000..52381f77 --- /dev/null +++ b/get_user_profile/src/authCodeWithPkce.ts @@ -0,0 +1,55 @@ +export async function redirectToAuthCodeFlow(clientId: string) { + const verifier = generateCodeVerifier(128); + const challenge = await generateCodeChallenge(verifier); + + localStorage.setItem("verifier", verifier); + + const params = new URLSearchParams(); + params.append("client_id", clientId); + params.append("response_type", "code"); + params.append("redirect_uri", "http://localhost:5173/callback"); + params.append("scope", "user-read-private user-read-email"); + params.append("code_challenge_method", "S256"); + params.append("code_challenge", challenge); + + document.location = `https://accounts.spotify.com/authorize?${params.toString()}`; +} + +export async function getAccessToken(clientId: string, code: string) { + const verifier = localStorage.getItem("verifier"); + + const params = new URLSearchParams(); + params.append("client_id", clientId); + params.append("grant_type", "authorization_code"); + params.append("code", code); + params.append("redirect_uri", "http://localhost:5173/callback"); + params.append("code_verifier", verifier!); + + const result = await fetch("https://accounts.spotify.com/api/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params + }); + + const { access_token } = await result.json(); + return access_token; +} + +function generateCodeVerifier(length: number) { + let text = ''; + let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +async function generateCodeChallenge(codeVerifier: string) { + const data = new TextEncoder().encode(codeVerifier); + const digest = await window.crypto.subtle.digest('SHA-256', data); + return btoa(String.fromCharCode.apply(null, [...new Uint8Array(digest)])) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} diff --git a/get_user_profile/src/script.ts b/get_user_profile/src/script.ts index d58f1a25..4311d7a4 100644 --- a/get_user_profile/src/script.ts +++ b/get_user_profile/src/script.ts @@ -1,27 +1,19 @@ // Because this is a literal single page application // we detect a callback from Spotify by checking for the hash fragment +import { redirectToAuthCodeFlow, getAccessToken } from "./authCodeWithPkce"; -const clientId = "your-client-id-here"; // Replace with your client id -const params = new URLSearchParams(window.location.hash.substring(1)); -const code = params.get("access_token"); +const clientId = "your_client_id"; +const params = new URLSearchParams(window.location.search); +const code = params.get("code"); if (!code) { redirectToAuthCodeFlow(clientId); } else { - const profile = await fetchProfile(code); + const accessToken = await getAccessToken(clientId, code); + const profile = await fetchProfile(accessToken); populateUI(profile); } -async function redirectToAuthCodeFlow(clientId: string) { - const params = new URLSearchParams(); - params.append("client_id", clientId); - params.append("response_type", "token"); - params.append("redirect_uri", "http://localhost:5173/callback"); - params.append("scope", "user-read-private user-read-email"); - - document.location = `https://accounts.spotify.com/authorize?${params.toString()}`; -} - async function fetchProfile(code: string): Promise { const result = await fetch("https://api.spotify.com/v1/me", { method: "GET", headers: { Authorization: `Bearer ${code}` } @@ -41,5 +33,3 @@ function populateUI(profile: UserProfile) { document.getElementById("url")!.setAttribute("href", profile.href); document.getElementById("imgUrl")!.innerText = profile.images[0].url; } - -export { };