From 250f6d6b5752c5e8d453a47751ea9f564d55b4dd Mon Sep 17 00:00:00 2001 From: jo Date: Mon, 27 Feb 2023 22:19:58 +0000 Subject: [PATCH 1/3] update get profile demo to use more secure auth flow --- get_user_profile/src/authCodeWithPkce.ts | 55 ++++++++++++++++++++++++ get_user_profile/src/script.ts | 24 +++-------- 2 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 get_user_profile/src/authCodeWithPkce.ts diff --git a/get_user_profile/src/authCodeWithPkce.ts b/get_user_profile/src/authCodeWithPkce.ts new file mode 100644 index 00000000..139d4c12 --- /dev/null +++ b/get_user_profile/src/authCodeWithPkce.ts @@ -0,0 +1,55 @@ +export async function redirectToAuth(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:3000/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): Promise { + 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:3000/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..14e5bf56 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 { redirectToAuth, 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); + redirectToAuth(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 { }; From ceb001756714adffb9994fc786a6ba2e438f54b2 Mon Sep 17 00:00:00 2001 From: jo Date: Mon, 27 Feb 2023 22:50:35 +0000 Subject: [PATCH 2/3] remove implicit type --- get_user_profile/src/authCodeWithPkce.ts | 8 ++++---- get_user_profile/src/script.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/get_user_profile/src/authCodeWithPkce.ts b/get_user_profile/src/authCodeWithPkce.ts index 139d4c12..52381f77 100644 --- a/get_user_profile/src/authCodeWithPkce.ts +++ b/get_user_profile/src/authCodeWithPkce.ts @@ -1,4 +1,4 @@ -export async function redirectToAuth(clientId: string) { +export async function redirectToAuthCodeFlow(clientId: string) { const verifier = generateCodeVerifier(128); const challenge = await generateCodeChallenge(verifier); @@ -7,7 +7,7 @@ export async function redirectToAuth(clientId: string) { const params = new URLSearchParams(); params.append("client_id", clientId); params.append("response_type", "code"); - params.append("redirect_uri", "http://localhost:3000/callback"); + 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); @@ -15,14 +15,14 @@ export async function redirectToAuth(clientId: string) { document.location = `https://accounts.spotify.com/authorize?${params.toString()}`; } -export async function getAccessToken(clientId: string, code: string): Promise { +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:3000/callback"); + params.append("redirect_uri", "http://localhost:5173/callback"); params.append("code_verifier", verifier!); const result = await fetch("https://accounts.spotify.com/api/token", { diff --git a/get_user_profile/src/script.ts b/get_user_profile/src/script.ts index 14e5bf56..4311d7a4 100644 --- a/get_user_profile/src/script.ts +++ b/get_user_profile/src/script.ts @@ -1,13 +1,13 @@ // Because this is a literal single page application // we detect a callback from Spotify by checking for the hash fragment -import { redirectToAuth, getAccessToken } from "./authCodeWithPkce"; +import { redirectToAuthCodeFlow, getAccessToken } from "./authCodeWithPkce"; const clientId = "your_client_id"; const params = new URLSearchParams(window.location.search); const code = params.get("code"); if (!code) { - redirectToAuth(clientId); + redirectToAuthCodeFlow(clientId); } else { const accessToken = await getAccessToken(clientId, code); const profile = await fetchProfile(accessToken); From 3270774ef48f1da79414149a8c7c13a6058047d3 Mon Sep 17 00:00:00 2001 From: jo Date: Tue, 28 Feb 2023 14:06:03 +0000 Subject: [PATCH 3/3] update readme --- get_user_profile/readme.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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.