Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ebeee9c

Browse files
committedOct 4, 2024
add nextjs google oauth guide
1 parent 68de4e9 commit ebeee9c

File tree

1 file changed

+233
-0
lines changed

1 file changed

+233
-0
lines changed
 
+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
---
2+
title: "Tutorial: Google OAuth in Next.js"
3+
---
4+
5+
# Tutorial: Google OAuth in Next.js
6+
7+
_Before starting, make sure you've created the session and cookie API outlined in the [Sessions](/sessions/overview) page._
8+
9+
An [example project](https://github.com/lucia-auth/example-nextjs-google-oauth) based on this tutorial is also available. You can clone the example locally or [open it in StackBlitz](https://stackblitz.com/github/lucia-auth/example-nextjs-google-oauth).
10+
11+
```
12+
git clone git@github.com:lucia-auth/example-nextjs-google-oauth.git
13+
```
14+
15+
## Create an OAuth App
16+
17+
Create an Google OAuth client on the Cloud Console. Set the redirect URI to `http://localhost:3000/login/google/callback`. Copy and paste the client ID and secret to your `.env` file.
18+
19+
```bash
20+
# .env
21+
GOOGLE_CLIENT_ID=""
22+
GOOGLE_CLIENT_SECRET=""
23+
```
24+
25+
## Update database
26+
27+
Update your user model to include the user's Google ID and username.
28+
29+
```ts
30+
interface User {
31+
id: number;
32+
googleId: string;
33+
name: string;
34+
}
35+
```
36+
37+
## Setup Arctic
38+
39+
```
40+
npm install arctic
41+
```
42+
43+
Initialize the Google provider with the client ID, client secret, and redirect URI.
44+
45+
```ts
46+
import { Google } from "arctic";
47+
48+
export const google = new Google(
49+
process.env.GOOGLE_CLIENT_ID,
50+
process.env.GOOGLE_CLIENT_SECRET,
51+
"http://localhost:3000/login/google/callback"
52+
);
53+
```
54+
55+
## Sign in page
56+
57+
Create `app/login/page.tsx` and add a basic sign in button, which should be a link to `/login/google`.
58+
59+
```tsx
60+
// app/login/page.tsx
61+
export default async function Page() {
62+
return (
63+
<>
64+
<h1>Sign in</h1>
65+
<a href="/login/google">Sign in with Google</a>
66+
</>
67+
);
68+
}
69+
```
70+
71+
## Create authorization URL
72+
73+
Create an API route in `pages/login/google/index.ts`. Generate a new state and code verifier, and create a new authorization URL. Add the `openid` and `profile` scope to have access to the user's profile later on. Store the state and code verifier, and redirect the user to the authorization URL. The user will be redirected to Google's sign in page.
74+
75+
```ts
76+
// app/login/google/route.ts
77+
import { generateState, generateCodeVerifier } from "arctic";
78+
import { google } from "@/lib/auth";
79+
import { cookies } from "next/headers";
80+
81+
export async function GET(): Promise<Response> {
82+
const state = generateState();
83+
const codeVerifier = generateCodeVerifier();
84+
const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]);
85+
86+
cookies().set("google_oauth_state", state, {
87+
path: "/",
88+
httpOnly: true,
89+
secure: process.env.NODE_ENV === "production",
90+
maxAge: 60 * 10, // 10 minutes
91+
sameSite: "lax"
92+
});
93+
cookies().set("google_code_verifier", codeVerifier, {
94+
path: "/",
95+
httpOnly: true,
96+
secure: process.env.NODE_ENV === "production",
97+
maxAge: 60 * 10, // 10 minutes
98+
sameSite: "lax"
99+
});
100+
101+
return new Response(null, {
102+
status: 302,
103+
headers: {
104+
Location: url.toString()
105+
}
106+
});
107+
}
108+
```
109+
110+
## Validate callback
111+
112+
Create an Route Handlers in `app/login/google/callback/route.ts` to handle the callback. Check that the state in the URL matches the one that's stored. Then, validate the authorization code and stored code verifier. If you passed the `openid` and `profile` scope, Google will return a token ID with the user's profile. Check if the user is already registered; if not, create a new user. Finally, create a new session and set the session cookie to complete the authentication process.
113+
114+
```ts
115+
// app/login/google/callback/route.ts
116+
import { generateSessionToken, createSession, setSessionTokenCookie } from "@/lib/session";
117+
import { google } from "@/lib/oauth";
118+
import { cookies } from "next/headers";
119+
120+
import type { OAuth2Tokens } from "arctic";
121+
122+
export async function GET(request: Request): Promise<Response> {
123+
const url = new URL(request.url);
124+
const code = url.searchParams.get("code");
125+
const state = url.searchParams.get("state");
126+
const storedState = cookies().get("google_oauth_state")?.value ?? null;
127+
const codeVerifier = cookies().get("google_code_verifier")?.value ?? null;
128+
if (code === null || state === null || storedState === null || codeVerifier === null) {
129+
return new Response(null, {
130+
status: 400
131+
});
132+
}
133+
if (state !== storedState) {
134+
return new Response(null, {
135+
status: 400
136+
});
137+
}
138+
139+
let tokens: OAuth2Tokens;
140+
try {
141+
tokens = await google.validateAuthorizationCode(code, codeVerifier);
142+
} catch (e) {
143+
// Invalid code or client credentials
144+
return new Response(null, {
145+
status: 400
146+
});
147+
}
148+
const claims = decodeIdToken(tokens.idToken());
149+
const googleUserId = claims.sub;
150+
const username = claims.name;
151+
152+
// TODO: Replace this with your own DB query.
153+
const existingUser = await getUserFromGoogleId(googleUserId);
154+
155+
if (existingUser !== null) {
156+
const token = generateSessionToken();
157+
const session = await createSession(token, existingUser.id);
158+
setSessionTokenCookie(token, session.expiresAt);
159+
return new Response(null, {
160+
status: 302,
161+
headers: {
162+
Location: "/"
163+
}
164+
});
165+
}
166+
167+
// TODO: Replace this with your own DB query.
168+
const user = await createUser(googleUserId, username);
169+
170+
const sessionToken = generateSessionToken();
171+
const session = await createSession(sessionToken, user.id);
172+
setSessionTokenCookie(token, session.expiresAt);
173+
return new Response(null, {
174+
status: 302,
175+
headers: {
176+
Location: "/"
177+
}
178+
});
179+
}
180+
```
181+
182+
## Validate requests
183+
184+
Use the `getCurrentUser()` function from the [Session cookies in Next.js](/sessions/cookies/nextjs) page to get the current user and session.
185+
186+
```tsx
187+
import { redirect } from "next/navigation";
188+
import { getCurrentUser } from "@/lib/session";
189+
190+
export default async function Page() {
191+
const { user } = await getCurrentUser();
192+
if (user === null) {
193+
return redirect("/login");
194+
}
195+
return <h1>Hi, {user.name}!</h1>;
196+
}
197+
```
198+
199+
## Sign out
200+
201+
Sign out users by invalidating their session. Make sure to remove the session cookie as well.
202+
203+
```tsx
204+
import { getCurrentSession, invalidateSession, deleteSessionTokenCookie } from "@/lib/session";
205+
import { redirect } from "next/navigation";
206+
import { cookies } from "next/headers";
207+
208+
export default async function Page() {
209+
return (
210+
<form action={logout}>
211+
<button>Sign out</button>
212+
</form>
213+
);
214+
}
215+
216+
async function logout(): Promise<ActionResult> {
217+
"use server";
218+
const { session } = await getCurrentSession();
219+
if (!session) {
220+
return {
221+
error: "Unauthorized"
222+
};
223+
}
224+
225+
await invalidateSession(session.id);
226+
deleteSessionTokenCookie();
227+
return redirect("/login");
228+
}
229+
230+
interface ActionResult {
231+
error: string | null;
232+
}
233+
```

0 commit comments

Comments
 (0)
Please sign in to comment.