Skip to content

Instantly share code, notes, and snippets.

@infomiho
Last active January 3, 2025 22:02
Show Gist options
  • Save infomiho/3c63de7d53aba59d6293bcb59501a029 to your computer and use it in GitHub Desktop.
Save infomiho/3c63de7d53aba59d6293bcb59501a029 to your computer and use it in GitHub Desktop.
Implementing custom OAuth provider with Wasp 0.14.1+ (Spotify in this case)
# Put some dummy values here
GOOGLE_CLIENT_ID=x
GOOGLE_CLIENT_SECRET=x
# Put your Spotify client ID and secret here
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
app spotifyOauth {
wasp: {
version: "^0.14.1"
},
title: "spotify-oauth",
client: {
rootComponent: import { App } from "@src/App",
},
auth: {
userEntity: User,
onAuthFailedRedirectTo: "/",
methods: {
// We had to enable at least one OAuth provider so Wasp would install the `arctic` package
google: {}
}
},
}
route RootRoute { path: "/", to: MainPage }
page MainPage {
component: import { MainPage } from "@src/MainPage",
}
api authWithSpotify {
httpRoute: (GET, "/auth/spotify"),
fn: import { authWithSpotify } from "@src/auth",
entities: []
}
api authWithSpotifyCallback {
httpRoute: (GET, "/auth/spotify/callback"),
fn: import { authWithSpotifyCallback } from "@src/auth",
entities: []
}
{
"name": "spotifyOauth",
"dependencies": {
"@tanstack/react-query-devtools": "^4.36.1",
"arctic": "^1.2.1",
"react": "^18.2.0",
"wasp": "file:.wasp/out/sdk/wasp",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/react": "^18.0.37",
"prisma": "4.16.2",
"typescript": "^5.1.0",
"vite": "^4.3.9"
}
}
datasource db {
provider = "sqlite"
// Wasp requires that the url is set to the DATABASE_URL environment variable.
url = env("DATABASE_URL")
}
// Wasp requires the `prisma-client-js` generator to be present.
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
name String
profilePicture String
}
import { AuthWithSpotify, AuthWithSpotifyCallback } from "wasp/server/api";
import { generateState, Spotify, SpotifyTokens } from "arctic";
import { config } from "wasp/server";
import { createUser, findAuthIdentity, ProviderName } from "wasp/auth/utils";
import * as z from "zod";
import { getRedirectUriForOneTimeCode, tokenStore } from "wasp/server/auth";
if (
!process.env.SPOTIFY_CLIENT_ID ||
!process.env.SPOTIFY_CLIENT_SECRET
) {
throw new Error(
"Please provide SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in .env.server file"
);
}
const clientId = process.env.SPOTIFY_CLIENT_ID;
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
const redirectURI = `${config.serverUrl}/auth/spotify/callback`;
const spotify = new Spotify(clientId, clientSecret, redirectURI);
// This is the handler for the `/auth/spotify` route
export const authWithSpotify: AuthWithSpotify = async (req, res) => {
const state = generateState();
const url: URL = await spotify.createAuthorizationURL(state, {
scopes: [],
});
res.redirect(url.toString());
};
// This is the handler for the `/auth/spotify/callback` route
export const authWithSpotifyCallback: AuthWithSpotifyCallback = async (
req,
res
) => {
const code = req.query.code as string;
const tokens: SpotifyTokens = await spotify.validateAuthorizationCode(code);
const spotifyUser = await getSpotifyUser(tokens.accessToken);
const providerId = {
// Hack to use `spotify` here
providerName: "spotify" as ProviderName,
providerUserId: spotifyUser.id,
};
// Check if user exists first
const existingUser = await findAuthIdentity(providerId);
if (existingUser) {
// Login
const authId = existingUser.authId;
return redirectWithOneTimeToken(authId, res);
} else {
// Create new user
const user = await createUser(
providerId,
JSON.stringify(spotifyUser),
// User fields
{
name: spotifyUser.display_name,
profilePicture: spotifyUser.images[1].url
}
);
const authId = user.auth.id;
return redirectWithOneTimeToken(authId, res);
}
};
async function redirectWithOneTimeToken(
authId: string,
res: Parameters<AuthWithSpotifyCallback>[1]
) {
const oneTimeCode = await tokenStore.createToken(authId)
// Redirects to Wasp's OAuth callback on the client side
return res.redirect(
getRedirectUriForOneTimeCode(oneTimeCode)
);
}
const spotifyUserSchema = z.object({
id: z.string(),
display_name: z.string(),
external_urls: z.object({
spotify: z.string(),
}),
images: z.array(
z.object({
url: z.string(),
height: z.number(),
width: z.number(),
})
),
});
type SpotifyUser = z.infer<typeof spotifyUserSchema>;
async function getSpotifyUser(accessToken: string): Promise<SpotifyUser> {
const response = await fetch("https://api.spotify.com/v1/me", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const spotifyUser = spotifyUserSchema.parse(await response.json());
return spotifyUser;
}
import { logout, useAuth } from "wasp/client/auth";
export const MainPage = () => {
const { data: user } = useAuth();
return (
<div className="container">
<main>
{user ? (
<p>
<img src={user.profilePicture} alt="profile" />
<br />
Logged in as {user.name}
<br />
<button onClick={logout}>Log out</button>
</p>
) : (
<p>Not logged in</p>
)}
<div className="buttons">
<a
className="button button-filled"
href="http://localhost:3001/auth/spotify"
>
Login with Spotify
</a>
</div>
</main>
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment