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