Created
July 15, 2024 12:25
-
-
Save jonz94/8c2d90b3510d39c34b047431cc53933a to your computer and use it in GitHub Desktop.
create-t3-app: next-auth login with YouTube channel account via YouTube OAuth for YouTube Data API v3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/server/auth.ts | |
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | |
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | |
import { DrizzleAdapter } from '@auth/drizzle-adapter' | |
import { getServerSession, type DefaultSession, type NextAuthOptions } from 'next-auth' | |
import { type Adapter } from 'next-auth/adapters' | |
import { env } from '~/env' | |
import { db } from '~/server/db' | |
import { accounts, sessions, users, verificationTokens } from '~/server/db/schema' | |
/** | |
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` | |
* object and keep type safety. | |
* | |
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation | |
*/ | |
declare module 'next-auth' { | |
interface Session extends DefaultSession { | |
user: { | |
id: string | |
// ...other properties | |
// role: UserRole; | |
} & DefaultSession['user'] | |
} | |
// interface User { | |
// // ...other properties | |
// // role: UserRole; | |
// } | |
} | |
/** | |
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc. | |
* | |
* @see https://next-auth.js.org/configuration/options | |
*/ | |
export const authOptions: NextAuthOptions = { | |
debug: env.NEXTAUTH_DEBUG, | |
callbacks: { | |
session: ({ session, user }) => ({ | |
...session, | |
user: { | |
...session.user, | |
id: user.id, | |
}, | |
}), | |
}, | |
adapter: DrizzleAdapter(db, { | |
usersTable: users, | |
accountsTable: accounts, | |
sessionsTable: sessions, | |
verificationTokensTable: verificationTokens, | |
}) as Adapter, | |
providers: [ | |
// ✨ YouTube OAuth for YouTube Data API v3 | |
{ | |
id: 'youtube', | |
name: 'YouTube', | |
type: 'oauth', | |
version: '2.0', | |
style: { | |
logo: '/google.svg', | |
bg: '#ececec', | |
text: '#000', | |
}, | |
clientId: env.GOOGLE_CLIENT_ID, | |
clientSecret: env.GOOGLE_CLIENT_SECRET, | |
authorization: { | |
url: 'https://accounts.google.com/o/oauth2/v2/auth', | |
params: { | |
prompt: 'consent', | |
access_type: 'offline', | |
response_type: 'code', | |
client_id: env.GOOGLE_CLIENT_ID, | |
scope: 'https://www.googleapis.com/auth/youtube.readonly', | |
}, | |
}, | |
token: { | |
async request(context) { | |
const response = await fetch('https://oauth2.googleapis.com/token', { | |
method: 'POST', | |
body: new URLSearchParams({ | |
code: context.params.code ?? '', | |
redirect_uri: context.provider.callbackUrl, | |
client_id: context.provider.clientId ?? '', | |
client_secret: context.provider.clientSecret ?? '', | |
scope: 'https://www.googleapis.com/auth/youtube.readonly', | |
grant_type: 'authorization_code', | |
}), | |
}).then((response) => response.json()) | |
return { | |
tokens: { | |
access_token: response.access_token, | |
expires_at: Math.floor(Date.now() / 1000) + response.expires_in, | |
refresh_token: response.refresh_token, | |
token_type: response.token_type, | |
scope: response.scope, | |
}, | |
} | |
}, | |
}, | |
userinfo: { | |
async request(context) { | |
const accessToken = context.tokens.access_token | |
if (accessToken === undefined) { | |
throw new Error('[request uesrinfo] access_token is empty') | |
} | |
// ✨ using https://developers.google.com/youtube/v3/docs/channels/list to get channel name and avatar image url | |
const result = await fetch( | |
'https://www.googleapis.com/youtube/v3/channels?part=id,snippet&maxResults=1&mine=true', | |
{ | |
headers: { | |
Authorization: `Bearer ${accessToken}`, | |
}, | |
}, | |
).then((response) => response.json()) | |
const youtubeChannelId = result.items[0]?.id ?? '' | |
return { | |
sub: youtubeChannelId, | |
name: result.items[0]?.snippet?.title, | |
// ⚠️ YouTube OAuth only returns the channel ID, not the email. | |
// (due to the fact that a single Google account can have multiple YouTube channels) | |
// ⚠️ we must put a fake email address here as a workaround, because next-auth requires the email field to be non-empty. | |
email: `${youtubeChannelId}@fake.email.address.com`, | |
image: result.items[0]?.snippet?.thumbnails?.high?.url, | |
} | |
}, | |
}, | |
profile(profile) { | |
return { | |
id: profile.sub, | |
name: profile.name, | |
email: `${profile.sub}@no-reply.youtube.com`, | |
image: profile.image, | |
} | |
}, | |
}, | |
], | |
} | |
/** | |
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. | |
* | |
* @see https://next-auth.js.org/configuration/nextjs | |
*/ | |
export const getServerAuthSession = () => getServerSession(authOptions) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment