-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathauth.config.ts
231 lines (206 loc) · 8.32 KB
/
auth.config.ts
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
import { cookies } from 'next/headers'
import { CredentialsSignin, type User, type NextAuthConfig } from 'next-auth'
import { type JWT } from 'next-auth/jwt'
import Credentials from 'next-auth/providers/credentials'
import Google from 'next-auth/providers/google'
import { z } from 'zod'
import { loginFormSchema } from '@/schemas/auth'
import { xhr } from '@/lib/http'
import { STATUS_TEXTS } from '@/lib/http-status-codes/en'
import type { LoginAPI, AuthTokenAPI } from '@/types/api'
import { isTokenExpired } from '@/lib/jose'
class CustomError extends CredentialsSignin {
constructor(code: string) {
super()
this.code = code
this.message = code
this.stack = undefined
}
}
// You can put all common configuration here which does not rely on the adapter.
// Notice this is exporting a configuration object only, we’re not calling NextAuth() here.
export const authConfig: NextAuthConfig = {
secret: process.env.AUTH_SECRET,
pages: {
signIn: '/auth/login',
signOut: '/auth/logout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
// newUser: '/auth/new-user', // New users will be directed here on first sign in (leave the property out if not of interest)
},
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
// Google requires "offline" access_type to provide a `refresh_token`
authorization: {
params: { access_type: 'offline', prompt: 'consent', response_type: 'code' },
},
// [ERROR] OAuthAccountNotLinked
// async profile(profile) { return profile },
}),
Credentials({
// You can specify which fields should be submitted, by adding keys to the `credentials` object.
// e.g. domain, username, password, 2FA token, etc.
credentials: {
email: {},
password: {},
rememberMe: {},
},
async authorize(credentials, req) {
const { data, success } = loginFormSchema.safeParse({
email: credentials?.email,
password: credentials?.password,
rememberMe: credentials?.rememberMe === 'true',
})
if (!success) {
throw new CustomError(STATUS_TEXTS.BAD_REQUEST)
}
try {
const {
message,
data: { user },
} = await xhr.post<LoginAPI>('/api/auth/login', {
body: JSON.stringify(data),
})
// No user found, so this is their first attempt to login
// Optionally, this is also the place you could do a user registration
if (!user) throw new CustomError(message)
// return user object with their profile data
return {
id: user.id,
role: user.role,
plan: user.plan,
access_token: user.access_token,
expires_at: user.expires_at,
refresh_token: user.refresh_token,
} as User
} catch (e: unknown) {
throw new CustomError((e as Error)?.message)
}
},
}),
// [ERROR] Module not found: Can't resolve 'crypto'
// Nodemailer({ server: process.env.EMAIL_SERVER, from: process.env.EMAIL_FROM }),
],
// By default, the `id` property does not exist on `token` or `session`. See the [TypeScript](https://authjs.dev/getting-started/typescript) on how to add it.
callbacks: {
async authorized({ request, auth }) {
// Logged in users are authenticated, otherwise redirect to login page
return !!auth
},
async signIn({ account, profile }) {
if (account?.provider === 'google') return !!profile?.email_verified
return true // Do different verification for other providers that don't have `email_verified`
},
// Using the `...rest` parameter to be able to narrow down the type based on `trigger`
async jwt({ token, user, account, trigger, session }) {
const isRememberMe = cookies().get('rememberMe')?.value === 'true'
// First-time login, save the `access_token`, its expiry and the `refresh_token`
if (account && user) {
token = { ...token, ...user } as JWT
if (account.type) token.type = account.type
if (account.provider) token.provider = account.provider
if (account.access_token) token.access_token = account.access_token
if (account.expires_at) token.expires_at = account.expires_at
if (account.refresh_token) token.refresh_token = account.refresh_token
if (!isRememberMe) token.refresh_token = undefined
return token
}
// Note, that `session` can be any arbitrary object, remember to validate it!
if (trigger === 'update' && session?.user) {
return { ...token, ...session?.user }
}
// If the token is 10 minutes before expiration time
if (isTokenExpired(token.expires_at, { expiresBefore: 60 * 10 })) {
return { ...token, error: 'AccessTokenExpired' }
}
// Subsequent logins, but the `access_token` is still valid
if (!isRememberMe) return token
// Access token has expired, try to update it
if (token?.provider === 'credentials') {
token = await credentialsToken(token)
} else if (token?.provider === 'google') {
token = await googleToken(token)
}
return token
},
// By default, the `id` property does not exist on `session`. See the [TypeScript](https://authjs.dev/getting-started/typescript) on how to add it.
async session({ session, token }) {
return {
...session,
user: token,
access_token: token.access_token,
expires_at: token.expires_at,
error: token.error,
}
},
},
// events: {
// createUser: async (message) => { /* user created */ },
// linkAccount: async (message) => { /* account (e.g. Twitter) linked to a user */ },
// session: async (message) => { /* session is active */ },
// signIn: async (message) => { /* on successful sign in */ },
// signOut: async (message) => { /* on signout */ },
// updateUser: async (message) => { /* user updated - e.g. their email was verified */ },
// },
}
async function credentialsToken(token: JWT): Promise<JWT> {
// Subsequent logins, but the `access_token` has expired, try to refresh it
if (!token.refresh_token) {
return { ...token, error: 'Missing refresh_token' }
}
try {
const {
message,
data: { tokens },
} = await xhr.post<AuthTokenAPI>('/api/auth/token', {
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: token.refresh_token,
}),
})
if (!tokens) throw new Error(message)
return { ...token, ...tokens }
} catch (e: unknown) {
// If we fail to refresh the token, return an error so we can handle it on the page
return { ...token, error: 'RefreshTokenError' }
}
}
async function googleToken(token: JWT): Promise<JWT> {
// Subsequent logins, but the `access_token` has expired, try to refresh it
if (!token.refresh_token) {
return { ...token, error: 'Missing refresh_token' }
}
try {
// The `token_endpoint` can be found in the provider's documentation. Or if they support OIDC,
// at their `/.well-known/openid-configuration` endpoint.
// i.e. https://accounts.google.com/.well-known/openid-configuration
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
client_id: process.env.AUTH_GOOGLE_ID!,
client_secret: process.env.AUTH_GOOGLE_SECRET!,
grant_type: 'refresh_token',
refresh_token: token.refresh_token,
}),
})
const tokensOrError = await res.json()
if (!res.ok) throw tokensOrError
const newTokens = tokensOrError as {
access_token: string
expires_in: number
refresh_token?: string
}
return {
...token,
access_token: newTokens.access_token,
expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
// Some providers only issue refresh tokens once, so preserve if we did not get a new one
refresh_token: newTokens.refresh_token ? newTokens.refresh_token : token.refresh_token,
}
} catch (e: unknown) {
// If we fail to refresh the token, return an error so we can handle it on the page
return { ...token, error: 'RefreshTokenError' }
}
}