-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth): add login with google base
- Loading branch information
Showing
19 changed files
with
2,343 additions
and
129 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,93 @@ | ||
import { generateSessionToken, createSession } from '@/auth/sessions'; | ||
import { google } from '@/auth/oauth'; | ||
import { cookies } from 'next/headers'; | ||
import { decodeIdToken } from 'arctic'; | ||
|
||
import type { OAuth2Tokens } from 'arctic'; | ||
import { setSessionTokenCookie } from '@/auth/cookies'; | ||
import { | ||
COOKIE_GOOGLE_CODE_VERIFIER, | ||
COOKIE_GOOGLE_OAUTH_STATE, | ||
} from '@/auth/constants'; | ||
import { createUser, findUserByGoogleId } from '@/db/repo/users'; | ||
|
||
export async function GET(request: Request): Promise<Response> { | ||
const url = new URL(request.url); | ||
const code = url.searchParams.get('code'); | ||
const state = url.searchParams.get('state'); | ||
const cookieStore = await cookies(); | ||
const storedState = cookieStore.get(COOKIE_GOOGLE_OAUTH_STATE)?.value ?? null; | ||
const codeVerifier = | ||
cookieStore.get(COOKIE_GOOGLE_CODE_VERIFIER)?.value ?? null; | ||
|
||
if ( | ||
code === null || | ||
state === null || | ||
storedState === null || | ||
codeVerifier === null | ||
) { | ||
return new Response(null, { | ||
status: 400, | ||
}); | ||
} | ||
|
||
if (state !== storedState) { | ||
return new Response(null, { | ||
status: 400, | ||
}); | ||
} | ||
|
||
let tokens: OAuth2Tokens; | ||
try { | ||
tokens = await google.validateAuthorizationCode(code, codeVerifier); | ||
} catch (e) { | ||
// Invalid code or client credentials | ||
return new Response(null, { | ||
status: 400, | ||
}); | ||
} | ||
|
||
const claims = decodeIdToken(tokens.idToken()) as { | ||
sub: string; | ||
name: string; | ||
email: string; | ||
picture: string; | ||
}; | ||
|
||
const googleUserId = claims.sub; | ||
const name = claims.name; | ||
const email = claims.email; | ||
const picture = claims.picture; | ||
|
||
const existingUser = await findUserByGoogleId(googleUserId); | ||
|
||
if (existingUser) { | ||
const sessionToken = generateSessionToken(); | ||
const session = await createSession(sessionToken, existingUser.id); | ||
await setSessionTokenCookie(sessionToken, session.expiresAt); | ||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
Location: '/', | ||
}, | ||
}); | ||
} | ||
|
||
const user = await createUser({ | ||
email, | ||
googleId: googleUserId, | ||
name, | ||
picture, | ||
}); | ||
|
||
const sessionToken = generateSessionToken(); | ||
const session = await createSession(sessionToken, user.id); | ||
await setSessionTokenCookie(sessionToken, session.expiresAt); | ||
|
||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
Location: '/', | ||
}, | ||
}); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,41 @@ | ||
// app/login/google/route.ts | ||
import { generateState, generateCodeVerifier } from 'arctic'; | ||
import { google } from '@/auth/oauth'; | ||
import { cookies } from 'next/headers'; | ||
import { | ||
COOKIE_GOOGLE_CODE_VERIFIER, | ||
COOKIE_GOOGLE_OAUTH_STATE, | ||
} from '@/auth/constants'; | ||
|
||
export async function GET(): Promise<Response> { | ||
const state = generateState(); | ||
const codeVerifier = generateCodeVerifier(); | ||
const url = google.createAuthorizationURL(state, codeVerifier, [ | ||
'openid', | ||
'profile', | ||
'email', | ||
]); | ||
|
||
const cookieStore = await cookies(); | ||
cookieStore.set(COOKIE_GOOGLE_OAUTH_STATE, state, { | ||
path: '/', | ||
httpOnly: true, | ||
secure: process.env.NODE_ENV === 'production', | ||
maxAge: 60 * 10, // 10 minutes | ||
sameSite: 'lax', | ||
}); | ||
cookieStore.set(COOKIE_GOOGLE_CODE_VERIFIER, codeVerifier, { | ||
path: '/', | ||
httpOnly: true, | ||
secure: process.env.NODE_ENV === 'production', | ||
maxAge: 60 * 10, // 10 minutes | ||
sameSite: 'lax', | ||
}); | ||
|
||
return new Response(null, { | ||
status: 302, | ||
headers: { | ||
Location: url.toString(), | ||
}, | ||
}); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,8 @@ | ||
export default function LoginPage() { | ||
return ( | ||
<> | ||
login page | ||
<a href="/login/google">Login with Google</a> | ||
</> | ||
); | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,101 1,16 @@ | ||
import Image from "next/image"; | ||
import { getCurrentSession } from '@/auth/sessions'; | ||
import { redirect } from 'next/navigation'; | ||
|
||
export default function Home() { | ||
return ( | ||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> | ||
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start"> | ||
<Image | ||
className="dark:invert" | ||
src="/next.svg" | ||
alt="Next.js logo" | ||
width={180} | ||
height={38} | ||
priority | ||
/> | ||
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]"> | ||
<li className="mb-2"> | ||
Get started by editing{" "} | ||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold"> | ||
app/page.tsx | ||
</code> | ||
. | ||
</li> | ||
<li>Save and see your changes instantly.</li> | ||
</ol> | ||
export default async function Home() { | ||
const { user } = await getCurrentSession(); | ||
if (user === null) { | ||
return redirect('/login'); | ||
} | ||
|
||
<div className="flex gap-4 items-center flex-col sm:flex-row"> | ||
<a | ||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5" | ||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
className="dark:invert" | ||
src="/vercel.svg" | ||
alt="Vercel logomark" | ||
width={20} | ||
height={20} | ||
/> | ||
Deploy now | ||
</a> | ||
<a | ||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44" | ||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
Read our docs | ||
</a> | ||
</div> | ||
</main> | ||
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center"> | ||
<a | ||
className="flex items-center gap-2 hover:underline hover:underline-offset-4" | ||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
aria-hidden | ||
src="/file.svg" | ||
alt="File icon" | ||
width={16} | ||
height={16} | ||
/> | ||
Learn | ||
</a> | ||
<a | ||
className="flex items-center gap-2 hover:underline hover:underline-offset-4" | ||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
aria-hidden | ||
src="/window.svg" | ||
alt="Window icon" | ||
width={16} | ||
height={16} | ||
/> | ||
Examples | ||
</a> | ||
<a | ||
className="flex items-center gap-2 hover:underline hover:underline-offset-4" | ||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
<Image | ||
aria-hidden | ||
src="/globe.svg" | ||
alt="Globe icon" | ||
width={16} | ||
height={16} | ||
/> | ||
Go to nextjs.org → | ||
</a> | ||
</footer> | ||
</div> | ||
return ( | ||
<> | ||
Hello page | ||
{user.name} | ||
</> | ||
); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,19 @@ | ||
'use server'; | ||
|
||
import { redirect } from 'next/navigation'; | ||
import { deleteSessionTokenCookie } from './cookies'; | ||
import { getCurrentSession, invalidateSession } from './sessions'; | ||
|
||
async function signOut() { | ||
const { session } = await getCurrentSession(); | ||
if (!session) { | ||
return { | ||
error: 'Unauthorized', | ||
}; | ||
} | ||
|
||
await invalidateSession(session.id); | ||
await deleteSessionTokenCookie(); | ||
|
||
return redirect('/login'); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,4 @@ | ||
export const COOKIE_SESSION = 'session'; | ||
|
||
export const COOKIE_GOOGLE_OAUTH_STATE = 'google_oauth_state'; | ||
export const COOKIE_GOOGLE_CODE_VERIFIER = 'google_code_verifier'; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,27 @@ | ||
import { cookies } from 'next/headers'; | ||
import { COOKIE_SESSION } from './constants'; | ||
|
||
export async function setSessionTokenCookie( | ||
token: string, | ||
expiresAt: Date | ||
): Promise<void> { | ||
const cookieStore = await cookies(); | ||
cookieStore.set(COOKIE_SESSION, token, { | ||
httpOnly: true, | ||
sameSite: 'lax', | ||
secure: process.env.NODE_ENV === 'production', | ||
expires: expiresAt, | ||
path: '/', | ||
}); | ||
} | ||
|
||
export async function deleteSessionTokenCookie(): Promise<void> { | ||
const cookieStore = await cookies(); | ||
cookieStore.set(COOKIE_SESSION, '', { | ||
httpOnly: true, | ||
sameSite: 'lax', | ||
secure: process.env.NODE_ENV === 'production', | ||
maxAge: 0, | ||
path: '/', | ||
}); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 1,8 @@ | ||
import { env } from '@/env'; | ||
import { Google } from 'arctic'; | ||
|
||
export const google = new Google( | ||
env.GOOGLE_CLIENT_ID, | ||
env.GOOGLE_CLIENT_SECRET, | ||
'http://localhost:3000/login/google/callback' | ||
); |
Oops, something went wrong.