Skip to content

Commit

Permalink
Replace Auth0 with NextAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewPrifer authored and AriaMinaei committed Oct 5, 2023
1 parent 6a3adce commit 96f51ec
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 159 deletions.
5 changes: 4 additions & 1 deletion packages/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ AUTH0_CLIENT_ID='client-id'
AUTH0_CLIENT_SECRET='client-secret'
# these are a public/private keypair. you can generate them via `$ yarn run cli dev generate-keypair`
STUDIO_AUTH_JWT_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCSl57bV4RJ3wDj4mDOi2UVM1Ga5+aqLnEkKGA81wx2GCdpJABFW+MC+MQ6H6Qbu/R3zLLx2KwT9RFKnfEPgQoif7tqdZWQnnS3LM3Q+0e0jsV6kKVlo6IIGDSZ2yS1LOCawbfyR6ZmWVbUlPEyz8zJF6i55rw62ekf0R9+oi6LzSzCmYmwcfS386BViC1JLA4Hks8OMQGtnoT7Vccs8gRzQUeauupfxqF3/eTPpdLPksMi6OozW5IfIpD4ppeA+eZ/EaQy1ANrzVzSQGqJTVu5H+ImMifQHlgNs5hVDhYtcfqB+sBlrI7v+3JgkX/UGGv0HirJqQJfl1I1Zhvuu+4zAgMBAAECggEAFaCjAk9uKAvrsLNkfvSX7EHPB2CxamhBrwj58d/0abP0lJGILLN60aRsJvsmFiVr0wTzXbUO5j7g1zZoK1ZpbV+VAgbpExYduCy3DN8V3DC4N/YBQPacYD9Z+10WlFhTpuFvxyIFDdKeeeyjjVCVMRH5hbviB3jA6T3MPL2myl84v164bXGXt7lO542IJ02iupcOLr0M4Qwty8OI8AUNz8Nes8DaIfiWPs3mzhpP7cvzaeHh47kpxZEnwkla2N9VqMi/mFgJkbbi6uYFQQVHTY4PZ0uddrU9MLBh3akxelpXtHyPcAa50god/JKmOLGHOQ8A4plYQ6lP+HtVVkZTGQKBgQDL/Kpxch7G6rklhH9AS7LEIoyg5FwWFJZ6O+a9obBQemHQiVUBan7+CqQGCUz/G460yaWTlKExi7s4o/kwNyyDc4YaxzpZ1eRqQlNpViQzkzbL9BakoNyzfWO0EvpOfCx5UITjfr05iRBFPWomt8fqzbUBQq7TaPBnj3pbMuNLxwKBgQC3+H3OxGaU8foZXFeKttclgpc5UL7ttpqeilcBMHFkNCYQtQHiooxx2mB5IcRLMCqt8+CPolPSODHNGzh69EdgPEb6vJnnLRq7mlvvxv2fAUHxRzwwzt0kJ73i4VS/10vbgjTP/hjumbp8T1NvyrHH5+TjRP35IwmhVywRmlzSNQKBgQCwSD4TpAes55PxNDu0GLb4gNL/B9n8yvXv1GxnXJ++LMTzLntjqeDtMczl2ovLusjsu4Z3r25OHu9A15O1czjosKEn6xwmHo4ytfbXhTXrzEECqIIY61tPUgEj0XK2+OCGRmtRHmnwmt6Qt65Qn2oJJJRT7oie8oUvOfMHdUawaQKBgBs5OFS6l3t++0V5drLeL6QrWPlwS3Cdzu+bBRj19DGhzeg3ANpqt2G5sQD70DJYJFiteBOJL+Ix0pzJZGg9cbp58P71ncip0gTk6KnoxmsbIojzw6JtWigZgW3rbkEdOOp4sBv/O+1C7meqzWwDkJ9GX4aKFRMi7i/j2G+aahXpAoGBAJlX1gVeE1j1D5LDOIKr56kAK4rsHYlleqYNs1iNnQy2rxA+rn0JWLuWRIn9Jvgl1RzPa5UY5CXMg5NlN+U6X2DFtGqpimBIay5Do5c5dqwpCpcckGiwW6mQ+uvbUcQDtko07t0CVtFlsv0WsckRLFf5qyNccdLZ5apQ3RzRYUrd-----END PRIVATE KEY-----
STUDIO_AUTH_JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkpee21eESd8A4+JgzotlFTNRmufmqi5xJChgPNcMdhgnaSQARVvjAvjEOh+kG7v0d8yy8disE/URSp3xD4EKIn+7anWVkJ50tyzN0PtHtI7FepClZaOiCBg0mdsktSzgmsG38kemZllW1JTxMs/MyReouea8OtnpH9EffqIui80swpmJsHH0t/OgVYgtSSwOB5LPDjEBrZ6E+1XHLPIEc0FHmrrqX8ahd/3kz6XSz5LDIujqM1uSHyKQ+KaXgPnmfxGkMtQDa81c0kBqiU1buR/iJjIn0B5YDbOYVQ4WLXH6gfrAZayO7/tyYJF/1Bhr9B4qyakCX5dSNWYb7rvuMwIDAQAB-----END PUBLIC KEY-----
STUDIO_AUTH_JWT_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkpee21eESd8A4+JgzotlFTNRmufmqi5xJChgPNcMdhgnaSQARVvjAvjEOh+kG7v0d8yy8disE/URSp3xD4EKIn+7anWVkJ50tyzN0PtHtI7FepClZaOiCBg0mdsktSzgmsG38kemZllW1JTxMs/MyReouea8OtnpH9EffqIui80swpmJsHH0t/OgVYgtSSwOB5LPDjEBrZ6E+1XHLPIEc0FHmrrqX8ahd/3kz6XSz5LDIujqM1uSHyKQ+KaXgPnmfxGkMtQDa81c0kBqiU1buR/iJjIn0B5YDbOYVQ4WLXH6gfrAZayO7/tyYJF/1Bhr9B4qyakCX5dSNWYb7rvuMwIDAQAB-----END PUBLIC KEY-----

# generate via openssl rand -base64 32
NEXTAUTH_SECRET='secret'
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@auth0/nextjs-auth0": "^3.1.0",
"@auth/prisma-adapter": "^1.0.3",
"@prisma/client": "^4.12.0",
"@tanstack/react-query": "^4.32.6",
"@trpc/client": "^10.38.0",
Expand All @@ -33,6 +33,7 @@
"jose": "^4.14.4",
"nanoid": "^3.3.1",
"next": "13.4.13",
"next-auth": "^4.23.2",
"npm-run-all": "^4.1.5",
"pg": "^8.11.2",
"prisma": "^4.12.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Warnings:
- You are about to drop the column `auth0Data` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `auth0Sid` on the `User` table. All the data in the column will be lost.
- A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "User_auth0Sid_key";

-- AlterTable
ALTER TABLE "User" DROP COLUMN "auth0Data",
DROP COLUMN "auth0Sid",
ADD COLUMN "emailVerified" TIMESTAMP(3),
ADD COLUMN "image" TEXT,
ADD COLUMN "name" TEXT,
ALTER COLUMN "email" DROP NOT NULL;

-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,

CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,

CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");

-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");

-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");

-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- AddForeignKey
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
50 changes: 44 additions & 6 deletions packages/app/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,55 @@ datasource db {
url = env("DATABASE_URL")
}

model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}

model User {
id String @id
auth0Sid String @unique
email String
projects Project[]
libSessions LibSession[]
auth0Data Json
projects Project[]
libSessions LibSession[]
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
@@index([email], name: "email")
}

model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

model Project {
id String @id
name String?
Expand Down
14 changes: 6 additions & 8 deletions packages/app/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import React from 'react'
import {UserProvider} from '@auth0/nextjs-auth0/client'
import {SessionProvider} from 'next-auth/react'
import type {AppProps} from 'next/app'

export default function App({
Component,
pageProps,
}: {
Component: React.ComponentType
pageProps: any
}) {
pageProps: {session, ...pageProps},
}: AppProps) {
return (
<UserProvider>
<SessionProvider session={session}>
<Component {...pageProps} />
</UserProvider>
</SessionProvider>
)
}
4 changes: 0 additions & 4 deletions packages/app/src/pages/api/auth/[...auth0].ts

This file was deleted.

4 changes: 4 additions & 0 deletions packages/app/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import NextAuth from 'next-auth'
import {nextAuthConfig} from 'src/utils/authUtils'

export default NextAuth(nextAuthConfig)
11 changes: 6 additions & 5 deletions packages/app/src/pages/api/studio-auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type {NextApiRequest, NextApiResponse} from 'next'
import prisma from 'src/prisma'
import {getSession} from '@auth0/nextjs-auth0'

import {ensureUserFromAuth0Session, studioAuth} from 'src/utils/authUtils'
import {getAppSession, studioAuth} from 'src/utils/authUtils'
import {userCodeLength} from 'src/trpc/routes/studioAuthRouter'

export default async function libAuth(
Expand Down Expand Up @@ -35,9 +34,11 @@ export default async function libAuth(
return
}

const session = await getSession(req, res)
const session = await getAppSession(req, res)

// if no session, redirect to login
if (!session || !session.user) {
res.redirect('/api/auth/login?returnTo=' + encodeURIComponent(req.url!))
res.redirect(`/api/auth/signin?callbackUrl=${encodeURIComponent(req.url!)}`)
return
}

Expand Down Expand Up @@ -72,7 +73,7 @@ export default async function libAuth(
return
}

const user = await ensureUserFromAuth0Session(req, res)
const user = session.user

const {refreshToken, accessToken} = await studioAuth.createSession(user)

Expand Down
13 changes: 7 additions & 6 deletions packages/app/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import {useUser} from '@auth0/nextjs-auth0/client'
import {useSession, signIn, signOut} from 'next-auth/react'

import Link from 'next/link'

export default function HomePage() {
Expand All @@ -12,13 +13,13 @@ export default function HomePage() {
}

const Profile: React.FC<{}> = () => {
const {user, error, isLoading} = useUser()
const {data: session, status} = useSession()
const user = session?.user

if (isLoading) return <div>Loading...</div>
if (error) return <div>{error.message}</div>
if (status === 'loading') return <div>Loading...</div>

if (!user) {
return <a href="/api/auth/login">log in</a>
return <button onClick={() => signIn('github')}>Sign in</button>
}

return (
Expand All @@ -27,7 +28,7 @@ const Profile: React.FC<{}> = () => {
<h2>{user.name}</h2>
<p>{user.email}</p>
<Link href="/projects">Projects</Link> <br />
<a href="/api/auth/logout">log out</a>
<button onClick={() => signOut()}>Sign out</button>
</div>
)
}
2 changes: 1 addition & 1 deletion packages/app/src/pages/projects/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {withPageAuthRequired} from '@auth0/nextjs-auth0/client'
import React, {useState} from 'react'
import useApi from '../../useApi'
import {withPageAuthRequired} from 'src/utils/withPageAuthRequired'

const ProjectsPage: React.FC<{}> = withPageAuthRequired(({}) => {
const {response, error, isLoading} = useApi('/api/projects')
Expand Down
77 changes: 41 additions & 36 deletions packages/app/src/utils/authUtils.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,53 @@
import type {NextApiRequest, NextApiResponse} from 'next'
import type {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
} from 'next'
import type {User} from '../../prisma/client-generated'
import prisma from '../prisma'
import {getSession} from '@auth0/nextjs-auth0'
import {v4} from 'uuid'
import * as jose from 'jose'
import type {$IntentionalAny} from 'src/types'
import {TRPCError} from '@trpc/server'
import {z} from 'zod'

/**
* Since Auth0 users are not stored in our database, we need to create a user in our database
* whenever we encounter a valid Auth0 session. If the user already exists, we return it.
*
* If the session is not valid, we throw an error.
*/
export async function ensureUserFromAuth0Session(
req: NextApiRequest,
res: NextApiResponse,
): Promise<User> {
const session = await getSession(req, res)
if (!session || !session.user) {
throw new Error(`User is not authenticated`)
import type {AuthOptions} from 'next-auth'
import {getServerSession} from 'next-auth'
import GithubProvider from 'next-auth/providers/github'
import {PrismaAdapter} from '@auth/prisma-adapter'
import type {Adapter} from 'next-auth/adapters'

// Extend NextAuth Session type to include all fields from the User model
declare module 'next-auth' {
interface Session {
user: User
}
}

const user = await prisma.user.findUnique({
where: {
auth0Sid: session.user.sid,
export const nextAuthConfig = {
// Why type assertion: https://github.com/nextauthjs/next-auth/issues/6106#issuecomment-1582582312
adapter: PrismaAdapter(prisma) as Adapter,
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
callbacks: {
session({session, token, user}) {
session.user = {...session.user, ...user}
return session
},
})

if (!user) {
const userId = v4() + v4()
const user = await prisma.user.create({
data: {
auth0Sid: session.user.sid,
email: session.user.email,
auth0Data: session.user,
id: userId,
},
})
return user
}

return user
},
} satisfies AuthOptions

// Use it in server contexts
export function getAppSession(
...args:
| [GetServerSidePropsContext['req'], GetServerSidePropsContext['res']]
| [NextApiRequest, NextApiResponse]
| []
) {
return getServerSession(...args, nextAuthConfig)
}

export type AccessTokenPayload = {
Expand All @@ -63,7 +68,7 @@ export namespace studioAuth {
const privateKey = await privateKeyPromise
const payload: AccessTokenPayload = {
userId: user.id,
email: user.email,
email: user.email ?? '',
}
const jwt = await new jose.SignJWT(payload)
.setProtectedHeader({alg: 'RS256'})
Expand Down
27 changes: 27 additions & 0 deletions packages/app/src/utils/withPageAuthRequired.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type {ComponentType} from 'react'
import {useEffect} from 'react'
import {useSession} from 'next-auth/react'

export const withPageAuthRequired = (Component: ComponentType) => {
return function WithPageAuthRequired(props: any): JSX.Element {
const {data: session, status} = useSession()

useEffect(() => {
if (session?.user || status === 'loading') return
let returnToPath: string

const currentLocation = window.location.toString()
returnToPath =
currentLocation.replace(new URL(currentLocation).origin, '') || '/'

window.location.assign(
`/api/auth/signin?callbackUrl=${encodeURIComponent(returnToPath)}`,
)
}, [session, status])

if (session?.user)
return <Component user={session.user} {...(props as any)} />

return <div>Redirecting...</div>
}
}
Loading

1 comment on commit 96f51ec

@vercel
Copy link

@vercel vercel bot commented on 96f51ec Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.