@cedarjs/auth-dbauth-setup 7.1.0 → 7.1.1-next.469

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,364 @@
1
+ import type { APIGatewayProxyEvent, Context } from 'aws-lambda'
2
+
3
+ import { DbAuthHandler } from '@cedarjs/auth-dbauth-api'
4
+ import type { DbAuthHandlerOptions, UserType } from '@cedarjs/auth-dbauth-api'
5
+ import {
6
+ OAuthHandler,
7
+ googleProvider,
8
+ githubProvider,
9
+ } from '@cedarjs/auth-dbauth-oauth'
10
+
11
+ import { cookieName } from 'src/lib/auth'
12
+ import { db } from 'src/lib/db'
13
+
14
+ // Any request whose path falls under this prefix is routed to the OAuth
15
+ // handler instead of the regular username/password dbAuth handler. This
16
+ // must match the callback URLs registered with each provider below (e.g.
17
+ // `${apiUrl}/auth/oauth/google/callback`).
18
+ const OAUTH_BASE_PATH = '/auth/oauth'
19
+
20
+ // The base URL of your deployed api side. Used to build the OAuth callback
21
+ // URLs registered with each provider's developer console. Update the
22
+ // production value once you know your deployed URL.
23
+ const apiUrl =
24
+ process.env.NODE_ENV === 'development'
25
+ ? 'http://localhost:8911'
26
+ : 'https://server.com'
27
+
28
+ const cookieAttributes = {
29
+ HttpOnly: true,
30
+ Path: '/',
31
+ SameSite: 'Lax' as const,
32
+ Secure: process.env.NODE_ENV !== 'development',
33
+
34
+ // If you need to allow other domains (besides the api side) access to
35
+ // the dbAuth session cookie:
36
+ // Domain: 'example.com',
37
+ }
38
+
39
+ export const handler = async (
40
+ event: APIGatewayProxyEvent,
41
+ context: Context
42
+ ) => {
43
+ if (event.path?.includes(OAUTH_BASE_PATH)) {
44
+ const oauthHandler = new OAuthHandler(event, context, {
45
+ // Provide prisma db client
46
+ db: db,
47
+
48
+ // The name of the property you'd call on `db` to access your user
49
+ // table. i.e. if your Prisma model is named `User` this value would
50
+ // be `user`, as in `db.user`
51
+ authModelAccessor: 'user',
52
+
53
+ // The name of the property you'd call on `db` to access your OAuth
54
+ // identity table. i.e. if your Prisma model is named `OAuth` this
55
+ // value would be `oAuth`, as in `db.oAuth`
56
+ oauthModelAccessor: 'oAuth',
57
+
58
+ // A map of what dbAuth calls a field to what your database calls it,
59
+ // matching the same fields used by `DbAuthHandler` below
60
+ authFields: {
61
+ id: 'id',
62
+ username: 'email',
63
+ hashedPassword: 'hashedPassword',
64
+ },
65
+
66
+ basePath: OAUTH_BASE_PATH,
67
+
68
+ // The configured OAuth providers, keyed by the path segment used in
69
+ // `/auth/oauth/{key}/...`. Get client id/secret values from each
70
+ // provider's developer console and set them in your `.env` file.
71
+ providers: {
72
+ // @oauth-provider:google
73
+ google: googleProvider({
74
+ clientId: process.env.GOOGLE_CLIENT_ID as string,
75
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
76
+ redirectUri: `${apiUrl}/auth/oauth/google/callback`,
77
+ }),
78
+ // @oauth-provider:google:end
79
+ // @oauth-provider:github
80
+ github: githubProvider({
81
+ clientId: process.env.GITHUB_CLIENT_ID as string,
82
+ clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
83
+ redirectUri: `${apiUrl}/auth/oauth/github/callback`,
84
+ }),
85
+ // @oauth-provider:github:end
86
+ },
87
+
88
+ // Where to send the browser after each flow completes
89
+ redirects: {
90
+ afterLogin: '/',
91
+ error: '/login',
92
+ },
93
+
94
+ signup: {
95
+ // handler() is invoked the first time a user signs in with a
96
+ // configured OAuth provider and no account is linked to that
97
+ // provider identity yet. `profile` is the canonical profile the
98
+ // provider returned (`providerUserId`, `email`, `username`, etc).
99
+ //
100
+ // At a minimum you need to create and return a user row. There's no
101
+ // password to store here since this account only ever signs in
102
+ // through the provider -- if your `User` model requires
103
+ // `hashedPassword`/`salt`, make sure those fields are optional (or
104
+ // have a default) so provider-only accounts can be created.
105
+ handler: ({ profile }) => {
106
+ // Some providers (e.g. GitHub, when the account keeps its email
107
+ // private) don't return an email at all. Customize this handler
108
+ // for any such provider you configure -- ask for an email on a
109
+ // follow-up screen, or derive a placeholder from a field you know
110
+ // is always present. Throwing here surfaces to the user as a
111
+ // `provider_error` redirect instead of silently creating an
112
+ // account with a fabricated address.
113
+ if (!profile.email) {
114
+ throw new Error(
115
+ `OAuth signup handler needs an email, but ${profile.providerUserId} didn't return one. Customize this handler to handle providers that don't supply an email.`,
116
+ )
117
+ }
118
+
119
+ return db.user.create({
120
+ data: {
121
+ email: profile.email,
122
+ },
123
+ })
124
+ },
125
+ },
126
+
127
+ // How long a user will remain logged in, in seconds
128
+ sessionExpires: 60 * 60 * 24 * 365 * 10,
129
+
130
+ // Specifies attributes on the cookie that dbAuth sets in order to
131
+ // remember who is logged in. See
132
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
133
+ cookie: {
134
+ attributes: cookieAttributes,
135
+ name: cookieName,
136
+ },
137
+ })
138
+
139
+ return await oauthHandler.invoke()
140
+ }
141
+
142
+ const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
143
+ // handler() is invoked after verifying that a user was found with the given
144
+ // username. This is where you can send the user an email with a link to
145
+ // reset their password.
146
+ //
147
+ // Whatever is returned from this function will be returned from
148
+ // the `forgotPassword()` function that is destructured from `useAuth()`.
149
+ // You could use this return value to, for example, show the email
150
+ // address in a toast message so the user will know it worked and where
151
+ // to look for the email.
152
+ //
153
+ // Note that this return value is sent to the client in *plain text*
154
+ // so don't include anything you wouldn't want prying eyes to see. The
155
+ // `user` here has been sanitized to only include the fields listed in
156
+ // `allowedUserFields` so it should be safe to return as-is.
157
+ handler: (user, _resetToken) => {
158
+ // TODO: Send an email/message to the user
159
+ // The message should include a link to reset their password with the
160
+ // `resetToken`. The URL should look something like:
161
+ // `http://localhost:8910/reset-password?resetToken=${resetToken}`
162
+ // When you implement this, change `_resetToken` to `resetToken` in the
163
+ // function arguments above.
164
+
165
+ return user
166
+ },
167
+
168
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
169
+ expires: 60 * 60 * 24,
170
+
171
+ errors: {
172
+ // for security reasons you may want to be vague here rather than expose
173
+ // the fact that the email address wasn't found (prevents fishing for
174
+ // valid email addresses)
175
+ usernameNotFound: 'Username not found',
176
+ // if the user somehow gets around client validation
177
+ usernameRequired: 'Username is required',
178
+ },
179
+ }
180
+
181
+ const loginOptions: DbAuthHandlerOptions['login'] = {
182
+ // handler() is called after finding the user that matches the
183
+ // username/password provided at login, but before actually considering them
184
+ // logged in. The `user` argument will be the user in the database that
185
+ // matched the username/password.
186
+ //
187
+ // If you want to allow this user to log in simply return the user.
188
+ //
189
+ // If you want to prevent someone logging in for another reason (maybe they
190
+ // didn't validate their email yet), throw an error and it will be returned
191
+ // by the `logIn()` function from `useAuth()` in the form of:
192
+ // `{ message: 'Error message' }`
193
+ handler: (user) => {
194
+ return user
195
+ },
196
+
197
+ errors: {
198
+ usernameOrPasswordMissing: 'Both username and password are required',
199
+ usernameNotFound: 'Username ${username} not found',
200
+ // For security reasons you may want to make this the same as the
201
+ // usernameNotFound error so that a malicious user can't use the error
202
+ // to narrow down if it's the username or password that's incorrect
203
+ incorrectPassword: 'Incorrect password for ${username}',
204
+ },
205
+
206
+ // How long a user will remain logged in, in seconds
207
+ expires: 60 * 60 * 24 * 365 * 10,
208
+ }
209
+
210
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
211
+ // handler() is invoked after the password has been successfully updated in
212
+ // the database. Returning anything truthy will automatically log the user
213
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
214
+ // user to the login page.
215
+ handler: (_user) => {
216
+ return true
217
+ },
218
+
219
+ // If `false` then the new password MUST be different from the current one
220
+ allowReusedPassword: true,
221
+
222
+ errors: {
223
+ // the resetToken is valid, but expired
224
+ resetTokenExpired: 'resetToken is expired',
225
+ // no user was found with the given resetToken
226
+ resetTokenInvalid: 'resetToken is invalid',
227
+ // the resetToken was not present in the URL
228
+ resetTokenRequired: 'resetToken is required',
229
+ // new password is the same as the old password (apparently they did not forget it)
230
+ reusedPassword: 'Must choose a new password',
231
+ },
232
+ }
233
+
234
+ const changePasswordOptions: DbAuthHandlerOptions['changePassword'] = {
235
+ // handler() is invoked after a logged in user has entered their current
236
+ // password correctly and the new password has been saved to the database.
237
+ // This is a good place to let the user know, by email for example, that
238
+ // their password was changed.
239
+ //
240
+ // Returning anything truthy keeps the user logged in. Return `false` to
241
+ // log them out, and in the Change Password page redirect the user to the
242
+ // login page.
243
+ handler: (_user) => {
244
+ // TODO: Send an email/message to the user letting them know that their
245
+ // password was changed. When you implement this, change `_user` to
246
+ // `user` in the function arguments above.
247
+
248
+ return true
249
+ },
250
+
251
+ // If `false` then the new password MUST be different from the current one
252
+ allowReusedPassword: false,
253
+
254
+ errors: {
255
+ // the current password was not provided
256
+ currentPasswordRequired: 'Current password is required',
257
+ // the new password was not provided
258
+ newPasswordRequired: 'New password is required',
259
+ // the current password is not correct
260
+ incorrectCurrentPassword: 'Current password is incorrect',
261
+ // the user doesn't have a password yet (they signed up with a third
262
+ // party login provider, for example)
263
+ passwordNotSet:
264
+ 'This account does not have a password. Use "Forgot password" to set one',
265
+ // new password is the same as the current password
266
+ reusedPassword: 'Must choose a new password',
267
+ },
268
+ }
269
+
270
+ interface UserAttributes {
271
+ name: string
272
+ }
273
+
274
+ const signupOptions: DbAuthHandlerOptions<
275
+ UserType,
276
+ UserAttributes
277
+ >['signup'] = {
278
+ // Whatever you want to happen to your data on new user signup. Cedar will
279
+ // check for duplicate usernames before calling this handler. At a minimum
280
+ // you need to save the `username`, `hashedPassword` and `salt` to your
281
+ // user table. `userAttributes` contains any additional object members that
282
+ // were included in the object given to the `signUp()` function you got
283
+ // from `useAuth()`.
284
+ //
285
+ // If you want the user to be immediately logged in, return the user that
286
+ // was created.
287
+ //
288
+ // If this handler throws an error, it will be returned by the `signUp()`
289
+ // function in the form of: `{ error: 'Error message' }`.
290
+ //
291
+ // If this returns anything else, it will be returned by the
292
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
293
+ handler: ({
294
+ username,
295
+ hashedPassword,
296
+ salt,
297
+ userAttributes: _userAttributes,
298
+ }) => {
299
+ return db.user.create({
300
+ data: {
301
+ email: username,
302
+ hashedPassword: hashedPassword,
303
+ salt: salt,
304
+ // name: userAttributes.name
305
+ },
306
+ })
307
+ },
308
+
309
+ // Include any format checks for password here. Return `true` if the
310
+ // password is valid, otherwise throw a `PasswordValidationError`.
311
+ // Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
312
+ passwordValidation: (_password) => {
313
+ return true
314
+ },
315
+
316
+ errors: {
317
+ // `field` will be either "username" or "password"
318
+ fieldMissing: '${field} is required',
319
+ usernameTaken: 'Username `${username}` already in use',
320
+ },
321
+ }
322
+
323
+ const authHandler = new DbAuthHandler(event, context, {
324
+ // Provide prisma db client
325
+ db: db,
326
+
327
+ // The name of the property you'd call on `db` to access your user table.
328
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
329
+ authModelAccessor: 'user',
330
+
331
+ // A map of what dbAuth calls a field to what your database calls it.
332
+ // `id` is whatever column you use to uniquely identify a user (probably
333
+ // something like `id` or `userId` or even `email`)
334
+ authFields: {
335
+ id: 'id',
336
+ username: 'email',
337
+ hashedPassword: 'hashedPassword',
338
+ salt: 'salt',
339
+ resetToken: 'resetToken',
340
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
341
+ },
342
+
343
+ // A list of fields on your user object that are safe to return to the
344
+ // client when invoking a handler that returns a user (like forgotPassword
345
+ // and signup). This list should be as small as possible to be sure not to
346
+ // leak any sensitive information to the client.
347
+ allowedUserFields: ['id', 'email'],
348
+
349
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
350
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
351
+ cookie: {
352
+ attributes: cookieAttributes,
353
+ name: cookieName,
354
+ },
355
+
356
+ changePassword: changePasswordOptions,
357
+ forgotPassword: forgotPasswordOptions,
358
+ login: loginOptions,
359
+ resetPassword: resetPasswordOptions,
360
+ signup: signupOptions,
361
+ })
362
+
363
+ return await authHandler.invoke()
364
+ }
@@ -102,6 +102,42 @@ export const handler = async (
102
102
  },
103
103
  }
104
104
 
105
+ const changePasswordOptions: DbAuthHandlerOptions['changePassword'] = {
106
+ // handler() is invoked after a logged in user has entered their current
107
+ // password correctly and the new password has been saved to the database.
108
+ // This is a good place to let the user know, by email for example, that
109
+ // their password was changed.
110
+ //
111
+ // Returning anything truthy keeps the user logged in. Return `false` to
112
+ // log them out, and in the Change Password page redirect the user to the
113
+ // login page.
114
+ handler: (_user) => {
115
+ // TODO: Send an email/message to the user letting them know that their
116
+ // password was changed. When you implement this, change `_user` to
117
+ // `user` in the function arguments above.
118
+
119
+ return true
120
+ },
121
+
122
+ // If `false` then the new password MUST be different from the current one
123
+ allowReusedPassword: false,
124
+
125
+ errors: {
126
+ // the current password was not provided
127
+ currentPasswordRequired: 'Current password is required',
128
+ // the new password was not provided
129
+ newPasswordRequired: 'New password is required',
130
+ // the current password is not correct
131
+ incorrectCurrentPassword: 'Current password is incorrect',
132
+ // the user doesn't have a password yet (they signed up with a third
133
+ // party login provider, for example)
134
+ passwordNotSet:
135
+ 'This account does not have a password. Use "Forgot password" to set one',
136
+ // new password is the same as the current password
137
+ reusedPassword: 'Must choose a new password',
138
+ },
139
+ }
140
+
105
141
  interface UserAttributes {
106
142
  name: string
107
143
  }
@@ -197,6 +233,7 @@ export const handler = async (
197
233
  name: cookieName,
198
234
  },
199
235
 
236
+ changePassword: changePasswordOptions,
200
237
  forgotPassword: forgotPasswordOptions,
201
238
  login: loginOptions,
202
239
  resetPassword: resetPasswordOptions,