@cedarjs/auth-dbauth-setup 0.0.4
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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/setup.d.ts +12 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +42 -0
- package/dist/setupData.d.ts +13 -0
- package/dist/setupData.d.ts.map +1 -0
- package/dist/setupData.js +40 -0
- package/dist/setupHandler.d.ts +7 -0
- package/dist/setupHandler.d.ts.map +1 -0
- package/dist/setupHandler.js +154 -0
- package/dist/shared.d.ts +11 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +91 -0
- package/dist/templates/api/functions/auth.ts.template +207 -0
- package/dist/templates/api/functions/auth.webAuthn.ts.template +225 -0
- package/dist/templates/api/lib/auth.ts.template +121 -0
- package/dist/templates/web/auth.rsc.ts.template +7 -0
- package/dist/templates/web/auth.ts.template +5 -0
- package/dist/templates/web/auth.webAuthn.rsc.ts.template +8 -0
- package/dist/templates/web/auth.webAuthn.ts.template +6 -0
- package/dist/webAuthn.setupData.d.ts +11 -0
- package/dist/webAuthn.setupData.d.ts.map +1 -0
- package/dist/webAuthn.setupData.js +62 -0
- package/package.json +47 -0
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
|
|
6
|
+
import { cookieName } from 'src/lib/auth'
|
|
7
|
+
import { db } from 'src/lib/db'
|
|
8
|
+
|
|
9
|
+
export const handler = async (
|
|
10
|
+
event: APIGatewayProxyEvent,
|
|
11
|
+
context: Context
|
|
12
|
+
) => {
|
|
13
|
+
const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
|
|
14
|
+
// handler() is invoked after verifying that a user was found with the given
|
|
15
|
+
// username. This is where you can send the user an email with a link to
|
|
16
|
+
// reset their password. With the default dbAuth routes and field names, the
|
|
17
|
+
// URL to reset the password will be:
|
|
18
|
+
//
|
|
19
|
+
// https://example.com/reset-password?resetToken=${user.resetToken}
|
|
20
|
+
//
|
|
21
|
+
// Whatever is returned from this function will be returned from
|
|
22
|
+
// the `forgotPassword()` function that is destructured from `useAuth()`.
|
|
23
|
+
// You could use this return value to, for example, show the email
|
|
24
|
+
// address in a toast message so the user will know it worked and where
|
|
25
|
+
// to look for the email.
|
|
26
|
+
//
|
|
27
|
+
// Note that this return value is sent to the client in *plain text*
|
|
28
|
+
// so don't include anything you wouldn't want prying eyes to see. The
|
|
29
|
+
// `user` here has been sanitized to only include the fields listed in
|
|
30
|
+
// `allowedUserFields` so it should be safe to return as-is.
|
|
31
|
+
handler: (user, _resetToken) => {
|
|
32
|
+
// TODO: Send user an email/message with a link to reset their password,
|
|
33
|
+
// including the `resetToken`. The URL should look something like:
|
|
34
|
+
// `http://localhost:8910/reset-password?resetToken=${resetToken}`
|
|
35
|
+
|
|
36
|
+
return user
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// How long the resetToken is valid for, in seconds (default is 24 hours)
|
|
40
|
+
expires: 60 * 60 * 24,
|
|
41
|
+
|
|
42
|
+
errors: {
|
|
43
|
+
// for security reasons you may want to be vague here rather than expose
|
|
44
|
+
// the fact that the email address wasn't found (prevents fishing for
|
|
45
|
+
// valid email addresses)
|
|
46
|
+
usernameNotFound: 'Username not found',
|
|
47
|
+
// if the user somehow gets around client validation
|
|
48
|
+
usernameRequired: 'Username is required',
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const loginOptions: DbAuthHandlerOptions['login'] = {
|
|
53
|
+
// handler() is called after finding the user that matches the
|
|
54
|
+
// username/password provided at login, but before actually considering them
|
|
55
|
+
// logged in. The `user` argument will be the user in the database that
|
|
56
|
+
// matched the username/password.
|
|
57
|
+
//
|
|
58
|
+
// If you want to allow this user to log in simply return the user.
|
|
59
|
+
//
|
|
60
|
+
// If you want to prevent someone logging in for another reason (maybe they
|
|
61
|
+
// didn't validate their email yet), throw an error and it will be returned
|
|
62
|
+
// by the `logIn()` function from `useAuth()` in the form of:
|
|
63
|
+
// `{ message: 'Error message' }`
|
|
64
|
+
handler: (user) => {
|
|
65
|
+
return user
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
errors: {
|
|
69
|
+
usernameOrPasswordMissing: 'Both username and password are required',
|
|
70
|
+
usernameNotFound: 'Username ${username} not found',
|
|
71
|
+
// For security reasons you may want to make this the same as the
|
|
72
|
+
// usernameNotFound error so that a malicious user can't use the error
|
|
73
|
+
// to narrow down if it's the username or password that's incorrect
|
|
74
|
+
incorrectPassword: 'Incorrect password for ${username}',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// How long a user will remain logged in, in seconds
|
|
78
|
+
expires: 60 * 60 * 24 * 365 * 10,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
|
|
82
|
+
// handler() is invoked after the password has been successfully updated in
|
|
83
|
+
// the database. Returning anything truthy will automatically log the user
|
|
84
|
+
// in. Return `false` otherwise, and in the Reset Password page redirect the
|
|
85
|
+
// user to the login page.
|
|
86
|
+
handler: (_user) => {
|
|
87
|
+
return true
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// If `false` then the new password MUST be different from the current one
|
|
91
|
+
allowReusedPassword: true,
|
|
92
|
+
|
|
93
|
+
errors: {
|
|
94
|
+
// the resetToken is valid, but expired
|
|
95
|
+
resetTokenExpired: 'resetToken is expired',
|
|
96
|
+
// no user was found with the given resetToken
|
|
97
|
+
resetTokenInvalid: 'resetToken is invalid',
|
|
98
|
+
// the resetToken was not present in the URL
|
|
99
|
+
resetTokenRequired: 'resetToken is required',
|
|
100
|
+
// new password is the same as the old password (apparently they did not forget it)
|
|
101
|
+
reusedPassword: 'Must choose a new password',
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface UserAttributes {
|
|
106
|
+
name: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const signupOptions: DbAuthHandlerOptions<
|
|
110
|
+
UserType,
|
|
111
|
+
UserAttributes
|
|
112
|
+
>['signup'] = {
|
|
113
|
+
// Whatever you want to happen to your data on new user signup. Redwood will
|
|
114
|
+
// check for duplicate usernames before calling this handler. At a minimum
|
|
115
|
+
// you need to save the `username`, `hashedPassword` and `salt` to your
|
|
116
|
+
// user table. `userAttributes` contains any additional object members that
|
|
117
|
+
// were included in the object given to the `signUp()` function you got
|
|
118
|
+
// from `useAuth()`.
|
|
119
|
+
//
|
|
120
|
+
// If you want the user to be immediately logged in, return the user that
|
|
121
|
+
// was created.
|
|
122
|
+
//
|
|
123
|
+
// If this handler throws an error, it will be returned by the `signUp()`
|
|
124
|
+
// function in the form of: `{ error: 'Error message' }`.
|
|
125
|
+
//
|
|
126
|
+
// If this returns anything else, it will be returned by the
|
|
127
|
+
// `signUp()` function in the form of: `{ message: 'String here' }`.
|
|
128
|
+
handler: ({
|
|
129
|
+
username,
|
|
130
|
+
hashedPassword,
|
|
131
|
+
salt,
|
|
132
|
+
userAttributes: _userAttributes,
|
|
133
|
+
}) => {
|
|
134
|
+
return db.user.create({
|
|
135
|
+
data: {
|
|
136
|
+
email: username,
|
|
137
|
+
hashedPassword: hashedPassword,
|
|
138
|
+
salt: salt,
|
|
139
|
+
// name: userAttributes.name
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Include any format checks for password here. Return `true` if the
|
|
145
|
+
// password is valid, otherwise throw a `PasswordValidationError`.
|
|
146
|
+
// Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
|
|
147
|
+
passwordValidation: (_password) => {
|
|
148
|
+
return true
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
errors: {
|
|
152
|
+
// `field` will be either "username" or "password"
|
|
153
|
+
fieldMissing: '${field} is required',
|
|
154
|
+
usernameTaken: 'Username `${username}` already in use',
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const authHandler = new DbAuthHandler(event, context, {
|
|
159
|
+
// Provide prisma db client
|
|
160
|
+
db: db,
|
|
161
|
+
|
|
162
|
+
// The name of the property you'd call on `db` to access your user table.
|
|
163
|
+
// i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
|
|
164
|
+
authModelAccessor: 'user',
|
|
165
|
+
|
|
166
|
+
// A map of what dbAuth calls a field to what your database calls it.
|
|
167
|
+
// `id` is whatever column you use to uniquely identify a user (probably
|
|
168
|
+
// something like `id` or `userId` or even `email`)
|
|
169
|
+
authFields: {
|
|
170
|
+
id: 'id',
|
|
171
|
+
username: 'email',
|
|
172
|
+
hashedPassword: 'hashedPassword',
|
|
173
|
+
salt: 'salt',
|
|
174
|
+
resetToken: 'resetToken',
|
|
175
|
+
resetTokenExpiresAt: 'resetTokenExpiresAt',
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// A list of fields on your user object that are safe to return to the
|
|
179
|
+
// client when invoking a handler that returns a user (like forgotPassword
|
|
180
|
+
// and signup). This list should be as small as possible to be sure not to
|
|
181
|
+
// leak any sensitive information to the client.
|
|
182
|
+
allowedUserFields: ['id', 'email'],
|
|
183
|
+
|
|
184
|
+
// Specifies attributes on the cookie that dbAuth sets in order to remember
|
|
185
|
+
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
|
|
186
|
+
cookie: {
|
|
187
|
+
attributes: {
|
|
188
|
+
HttpOnly: true,
|
|
189
|
+
Path: '/',
|
|
190
|
+
SameSite: 'Lax',
|
|
191
|
+
Secure: process.env.NODE_ENV !== 'development',
|
|
192
|
+
|
|
193
|
+
// If you need to allow other domains (besides the api side) access to
|
|
194
|
+
// the dbAuth session cookie:
|
|
195
|
+
// Domain: 'example.com',
|
|
196
|
+
},
|
|
197
|
+
name: cookieName,
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
forgotPassword: forgotPasswordOptions,
|
|
201
|
+
login: loginOptions,
|
|
202
|
+
resetPassword: resetPasswordOptions,
|
|
203
|
+
signup: signupOptions,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return await authHandler.invoke()
|
|
207
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
|
|
6
|
+
import { cookieName } from 'src/lib/auth'
|
|
7
|
+
import { db } from 'src/lib/db'
|
|
8
|
+
|
|
9
|
+
export const handler = async (
|
|
10
|
+
event: APIGatewayProxyEvent,
|
|
11
|
+
context: Context
|
|
12
|
+
) => {
|
|
13
|
+
const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = {
|
|
14
|
+
// handler() is invoked after verifying that a user was found with the given
|
|
15
|
+
// username. This is where you can send the user an email with a link to
|
|
16
|
+
// reset their password. With the default dbAuth routes and field names, the
|
|
17
|
+
// URL to reset the password will be:
|
|
18
|
+
//
|
|
19
|
+
// https://example.com/reset-password?resetToken=${user.resetToken}
|
|
20
|
+
//
|
|
21
|
+
// Whatever is returned from this function will be returned from
|
|
22
|
+
// the `forgotPassword()` function that is destructured from `useAuth()`
|
|
23
|
+
// You could use this return value to, for example, show the email
|
|
24
|
+
// address in a toast message so the user will know it worked and where
|
|
25
|
+
// to look for the email.
|
|
26
|
+
handler: (user) => {
|
|
27
|
+
return user
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// How long the resetToken is valid for, in seconds (default is 24 hours)
|
|
31
|
+
expires: 60 * 60 * 24,
|
|
32
|
+
|
|
33
|
+
errors: {
|
|
34
|
+
// for security reasons you may want to be vague here rather than expose
|
|
35
|
+
// the fact that the email address wasn't found (prevents fishing for
|
|
36
|
+
// valid email addresses)
|
|
37
|
+
usernameNotFound: 'Username not found',
|
|
38
|
+
// if the user somehow gets around client validation
|
|
39
|
+
usernameRequired: 'Username is required',
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const loginOptions: DbAuthHandlerOptions['login'] = {
|
|
44
|
+
// handler() is called after finding the user that matches the
|
|
45
|
+
// username/password provided at login, but before actually considering them
|
|
46
|
+
// logged in. The `user` argument will be the user in the database that
|
|
47
|
+
// matched the username/password.
|
|
48
|
+
//
|
|
49
|
+
// If you want to allow this user to log in simply return the user.
|
|
50
|
+
//
|
|
51
|
+
// If you want to prevent someone logging in for another reason (maybe they
|
|
52
|
+
// didn't validate their email yet), throw an error and it will be returned
|
|
53
|
+
// by the `logIn()` function from `useAuth()` in the form of:
|
|
54
|
+
// `{ message: 'Error message' }`
|
|
55
|
+
handler: (user) => {
|
|
56
|
+
return user
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
errors: {
|
|
60
|
+
usernameOrPasswordMissing: 'Both username and password are required',
|
|
61
|
+
usernameNotFound: 'Username ${username} not found',
|
|
62
|
+
// For security reasons you may want to make this the same as the
|
|
63
|
+
// usernameNotFound error so that a malicious user can't use the error
|
|
64
|
+
// to narrow down if it's the username or password that's incorrect
|
|
65
|
+
incorrectPassword: 'Incorrect password for ${username}',
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// How long a user will remain logged in, in seconds
|
|
69
|
+
expires: 60 * 60 * 24 * 365 * 10,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
|
|
73
|
+
// handler() is invoked after the password has been successfully updated in
|
|
74
|
+
// the database. Returning anything truthy will automatically logs the user
|
|
75
|
+
// in. Return `false` otherwise, and in the Reset Password page redirect the
|
|
76
|
+
// user to the login page.
|
|
77
|
+
handler: (_user) => {
|
|
78
|
+
return true
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// If `false` then the new password MUST be different than the current one
|
|
82
|
+
allowReusedPassword: true,
|
|
83
|
+
|
|
84
|
+
errors: {
|
|
85
|
+
// the resetToken is valid, but expired
|
|
86
|
+
resetTokenExpired: 'resetToken is expired',
|
|
87
|
+
// no user was found with the given resetToken
|
|
88
|
+
resetTokenInvalid: 'resetToken is invalid',
|
|
89
|
+
// the resetToken was not present in the URL
|
|
90
|
+
resetTokenRequired: 'resetToken is required',
|
|
91
|
+
// new password is the same as the old password (apparently they did not forget it)
|
|
92
|
+
reusedPassword: 'Must choose a new password',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface UserAttributes {
|
|
97
|
+
name: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const signupOptions: DbAuthHandlerOptions<
|
|
101
|
+
UserType,
|
|
102
|
+
UserAttributes
|
|
103
|
+
>['signup'] = {
|
|
104
|
+
// Whatever you want to happen to your data on new user signup. Redwood will
|
|
105
|
+
// check for duplicate usernames before calling this handler. At a minimum
|
|
106
|
+
// you need to save the `username`, `hashedPassword` and `salt` to your
|
|
107
|
+
// user table. `userAttributes` contains any additional object members that
|
|
108
|
+
// were included in the object given to the `signUp()` function you got
|
|
109
|
+
// from `useAuth()`.
|
|
110
|
+
//
|
|
111
|
+
// If you want the user to be immediately logged in, return the user that
|
|
112
|
+
// was created.
|
|
113
|
+
//
|
|
114
|
+
// If this handler throws an error, it will be returned by the `signUp()`
|
|
115
|
+
// function in the form of: `{ error: 'Error message' }`.
|
|
116
|
+
//
|
|
117
|
+
// If this returns anything else, it will be returned by the
|
|
118
|
+
// `signUp()` function in the form of: `{ message: 'String here' }`.
|
|
119
|
+
handler: ({
|
|
120
|
+
username,
|
|
121
|
+
hashedPassword,
|
|
122
|
+
salt,
|
|
123
|
+
userAttributes: _userAttributes,
|
|
124
|
+
}) => {
|
|
125
|
+
return db.user.create({
|
|
126
|
+
data: {
|
|
127
|
+
email: username,
|
|
128
|
+
hashedPassword: hashedPassword,
|
|
129
|
+
salt: salt,
|
|
130
|
+
// name: userAttributes.name
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Include any format checks for password here. Return `true` if the
|
|
136
|
+
// password is valid, otherwise throw a `PasswordValidationError`.
|
|
137
|
+
// Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
|
|
138
|
+
passwordValidation: (_password) => {
|
|
139
|
+
return true
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
errors: {
|
|
143
|
+
// `field` will be either "username" or "password"
|
|
144
|
+
fieldMissing: '${field} is required',
|
|
145
|
+
usernameTaken: 'Username `${username}` already in use',
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const authHandler = new DbAuthHandler(event, context, {
|
|
150
|
+
// Provide prisma db client
|
|
151
|
+
db: db,
|
|
152
|
+
|
|
153
|
+
// The name of the property you'd call on `db` to access your user table.
|
|
154
|
+
// ie. if your Prisma model is named `User` this value would be `user`, as in `db.user`
|
|
155
|
+
authModelAccessor: 'user',
|
|
156
|
+
|
|
157
|
+
// The name of the property you'd call on `db` to access your user credentials table.
|
|
158
|
+
// ie. if your Prisma model is named `UserCredential` this value would be `userCredential`, as in `db.userCredential`
|
|
159
|
+
credentialModelAccessor: 'userCredential',
|
|
160
|
+
|
|
161
|
+
// A map of what dbAuth calls a field to what your database calls it.
|
|
162
|
+
// `id` is whatever column you use to uniquely identify a user (probably
|
|
163
|
+
// something like `id` or `userId` or even `email`)
|
|
164
|
+
authFields: {
|
|
165
|
+
id: 'id',
|
|
166
|
+
username: 'email',
|
|
167
|
+
hashedPassword: 'hashedPassword',
|
|
168
|
+
salt: 'salt',
|
|
169
|
+
resetToken: 'resetToken',
|
|
170
|
+
resetTokenExpiresAt: 'resetTokenExpiresAt',
|
|
171
|
+
challenge: 'webAuthnChallenge',
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Specifies attributes on the cookie that dbAuth sets in order to remember
|
|
175
|
+
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
|
|
176
|
+
cookie: {
|
|
177
|
+
attributes: {
|
|
178
|
+
HttpOnly: true,
|
|
179
|
+
Path: '/',
|
|
180
|
+
SameSite: 'Lax',
|
|
181
|
+
Secure: process.env.NODE_ENV !== 'development' ? true : false,
|
|
182
|
+
|
|
183
|
+
// If you need to allow other domains (besides the api side) access to
|
|
184
|
+
// the dbAuth session cookie:
|
|
185
|
+
// Domain: 'example.com',
|
|
186
|
+
},
|
|
187
|
+
name: cookieName,
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
forgotPassword: forgotPasswordOptions,
|
|
191
|
+
login: loginOptions,
|
|
192
|
+
resetPassword: resetPasswordOptions,
|
|
193
|
+
signup: signupOptions,
|
|
194
|
+
|
|
195
|
+
// See https://redwoodjs.com/docs/auth/dbauth#webauthn for options
|
|
196
|
+
webAuthn: {
|
|
197
|
+
enabled: true,
|
|
198
|
+
// How long to allow re-auth via WebAuthn in seconds (default is 10 years).
|
|
199
|
+
// The `login.expires` time denotes how many seconds before a user will be
|
|
200
|
+
// logged out, and this value is how long they'll be to continue to use a
|
|
201
|
+
// fingerprint/face scan to log in again. When this one expires they
|
|
202
|
+
// *must* re-enter username and password to authenticate (WebAuthn will
|
|
203
|
+
// then be re-enabled for this amount of time).
|
|
204
|
+
expires: 60 * 60 * 24 * 365 * 10,
|
|
205
|
+
name: 'Redwood Application',
|
|
206
|
+
domain:
|
|
207
|
+
process.env.NODE_ENV === 'development' ? 'localhost' : 'server.com',
|
|
208
|
+
origin:
|
|
209
|
+
process.env.NODE_ENV === 'development'
|
|
210
|
+
? 'http://localhost:8910'
|
|
211
|
+
: 'https://server.com',
|
|
212
|
+
type: 'platform',
|
|
213
|
+
timeout: 60000,
|
|
214
|
+
credentialFields: {
|
|
215
|
+
id: 'id',
|
|
216
|
+
userId: 'userId',
|
|
217
|
+
publicKey: 'publicKey',
|
|
218
|
+
transports: 'transports',
|
|
219
|
+
counter: 'counter',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return await authHandler.invoke()
|
|
225
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Decoded } from '@cedarjs/api'
|
|
2
|
+
import { AuthenticationError, ForbiddenError } from '@cedarjs/graphql-server'
|
|
3
|
+
|
|
4
|
+
import { db } from './db'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The name of the cookie that dbAuth sets
|
|
8
|
+
*
|
|
9
|
+
* %port% will be replaced with the port the api server is running on.
|
|
10
|
+
* If you have multiple RW apps running on the same host, you'll need to
|
|
11
|
+
* make sure they all use unique cookie names
|
|
12
|
+
*/
|
|
13
|
+
export const cookieName = 'session_%port%'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The session object sent in as the first argument to getCurrentUser() will
|
|
17
|
+
* have a single key `id` containing the unique ID of the logged in user
|
|
18
|
+
* (whatever field you set as `authFields.id` in your auth function config).
|
|
19
|
+
* You'll need to update the call to `db` below if you use a different model
|
|
20
|
+
* name or unique field name, for example:
|
|
21
|
+
*
|
|
22
|
+
* return await db.profile.findUnique({ where: { email: session.id } })
|
|
23
|
+
* ───┬─── ──┬──
|
|
24
|
+
* model accessor ─┘ unique id field name ─┘
|
|
25
|
+
*
|
|
26
|
+
* !! BEWARE !! Anything returned from this function will be available to the
|
|
27
|
+
* client--it becomes the content of `currentUser` on the web side (as well as
|
|
28
|
+
* `context.currentUser` on the api side). You should carefully add additional
|
|
29
|
+
* fields to the `select` object below once you've decided they are safe to be
|
|
30
|
+
* seen if someone were to open the Web Inspector in their browser.
|
|
31
|
+
*/
|
|
32
|
+
export const getCurrentUser = async (session: Decoded) => {
|
|
33
|
+
if (!session || typeof session.id !== 'number') {
|
|
34
|
+
throw new Error('Invalid session')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return await db.user.findUnique({
|
|
38
|
+
where: { id: session.id },
|
|
39
|
+
select: { id: true },
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The user is authenticated if there is a currentUser in the context
|
|
45
|
+
*
|
|
46
|
+
* @returns {boolean} - If the currentUser is authenticated
|
|
47
|
+
*/
|
|
48
|
+
export const isAuthenticated = (): boolean => {
|
|
49
|
+
return !!context.currentUser
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* When checking role membership, roles can be a single value, a list, or none.
|
|
54
|
+
* You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client`
|
|
55
|
+
*/
|
|
56
|
+
type AllowedRoles = string | string[] | undefined
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks if the currentUser is authenticated (and assigned one of the given roles)
|
|
60
|
+
*
|
|
61
|
+
* @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles
|
|
62
|
+
*
|
|
63
|
+
* @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles,
|
|
64
|
+
* or when no roles are provided to check against. Otherwise returns false.
|
|
65
|
+
*/
|
|
66
|
+
export const hasRole = (roles: AllowedRoles): boolean => {
|
|
67
|
+
if (!isAuthenticated()) {
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const currentUserRoles = context.currentUser?.roles
|
|
72
|
+
|
|
73
|
+
if (typeof roles === 'string') {
|
|
74
|
+
if (typeof currentUserRoles === 'string') {
|
|
75
|
+
// roles to check is a string, currentUser.roles is a string
|
|
76
|
+
return currentUserRoles === roles
|
|
77
|
+
} else if (Array.isArray(currentUserRoles)) {
|
|
78
|
+
// roles to check is a string, currentUser.roles is an array
|
|
79
|
+
return currentUserRoles?.some((allowedRole) => roles === allowedRole)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Array.isArray(roles)) {
|
|
84
|
+
if (Array.isArray(currentUserRoles)) {
|
|
85
|
+
// roles to check is an array, currentUser.roles is an array
|
|
86
|
+
return currentUserRoles?.some((allowedRole) =>
|
|
87
|
+
roles.includes(allowedRole)
|
|
88
|
+
)
|
|
89
|
+
} else if (typeof currentUserRoles === 'string') {
|
|
90
|
+
// roles to check is an array, currentUser.roles is a string
|
|
91
|
+
return roles.some((allowedRole) => currentUserRoles === allowedRole)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// roles not found
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Use requireAuth in your services to check that a user is logged in,
|
|
101
|
+
* whether or not they are assigned a role, and optionally raise an
|
|
102
|
+
* error if they're not.
|
|
103
|
+
*
|
|
104
|
+
* @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access.
|
|
105
|
+
*
|
|
106
|
+
* @returns - If the currentUser is authenticated (and assigned one of the given roles)
|
|
107
|
+
*
|
|
108
|
+
* @throws {@link AuthenticationError} - If the currentUser is not authenticated
|
|
109
|
+
* @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions
|
|
110
|
+
*
|
|
111
|
+
* @see https://github.com/cedarjs/cedar/tree/main/packages/auth for examples
|
|
112
|
+
*/
|
|
113
|
+
export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => {
|
|
114
|
+
if (!isAuthenticated()) {
|
|
115
|
+
throw new AuthenticationError("You don't have permission to do that.")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (roles && !hasRole(roles)) {
|
|
119
|
+
throw new ForbiddenError("You don't have access to do that.")
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createDbAuthClient, createAuth } from '@cedarjs/auth-dbauth-web'
|
|
4
|
+
import WebAuthnClient from '@cedarjs/auth-dbauth-web/webAuthn'
|
|
5
|
+
|
|
6
|
+
const dbAuthClient = createDbAuthClient({ webAuthn: new WebAuthnClient() })
|
|
7
|
+
|
|
8
|
+
export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createDbAuthClient, createAuth } from '@cedarjs/auth-dbauth-web'
|
|
2
|
+
import WebAuthnClient from '@cedarjs/auth-dbauth-web/webAuthn'
|
|
3
|
+
|
|
4
|
+
const dbAuthClient = createDbAuthClient({ webAuthn: new WebAuthnClient() })
|
|
5
|
+
|
|
6
|
+
export const { AuthProvider, useAuth } = createAuth(dbAuthClient)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthGeneratorCtx } from '@cedarjs/cli-helpers/src/auth/authTasks.js';
|
|
2
|
+
export { extraTask } from './setupData';
|
|
3
|
+
export declare const webPackages: string[];
|
|
4
|
+
export declare const apiPackages: string[];
|
|
5
|
+
export declare const createUserModelTask: {
|
|
6
|
+
title: string;
|
|
7
|
+
task: (ctx: AuthGeneratorCtx) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export declare const notes: string[];
|
|
10
|
+
export declare const noteGenerate: string[];
|
|
11
|
+
//# sourceMappingURL=webAuthn.setupData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webAuthn.setupData.d.ts","sourceRoot":"","sources":["../src/webAuthn.setupData.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAA;AAKlF,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGvC,eAAO,MAAM,WAAW,UAAiC,CAAA;AAGzD,eAAO,MAAM,WAAW,UAAgC,CAAA;AAExD,eAAO,MAAM,mBAAmB;;gBAEZ,gBAAgB;CA+BnC,CAAA;AAGD,eAAO,MAAM,KAAK,UAoEjB,CAAA;AAED,eAAO,MAAM,YAAY,UAMxB,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _Object$defineProperty = require("@babel/runtime-corejs3/core-js/object/define-property");
|
|
4
|
+
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
|
|
5
|
+
_Object$defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
exports.createUserModelTask = exports.apiPackages = void 0;
|
|
9
|
+
_Object$defineProperty(exports, "extraTask", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: function () {
|
|
12
|
+
return _setupData.extraTask;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
exports.webPackages = exports.notes = exports.noteGenerate = void 0;
|
|
16
|
+
var _path = _interopRequireDefault(require("path"));
|
|
17
|
+
var _cliHelpers = require("@cedarjs/cli-helpers");
|
|
18
|
+
var _shared = require("./shared");
|
|
19
|
+
var _setupData = require("./setupData");
|
|
20
|
+
// copy some identical values from dbAuth provider
|
|
21
|
+
|
|
22
|
+
// required packages to install on the web side
|
|
23
|
+
const webPackages = exports.webPackages = ['@simplewebauthn/browser@^7'];
|
|
24
|
+
|
|
25
|
+
// required packages to install on the api side
|
|
26
|
+
const apiPackages = exports.apiPackages = ['@simplewebauthn/server@^7'];
|
|
27
|
+
const createUserModelTask = exports.createUserModelTask = {
|
|
28
|
+
title: 'Creating model `User`...',
|
|
29
|
+
task: async ctx => {
|
|
30
|
+
const hasUserModel = await (0, _shared.hasModel)('User');
|
|
31
|
+
if (hasUserModel && !ctx.force) {
|
|
32
|
+
throw new Error('User model already exists');
|
|
33
|
+
}
|
|
34
|
+
(0, _shared.addModels)(`
|
|
35
|
+
model User {
|
|
36
|
+
id Int @id @default(autoincrement())
|
|
37
|
+
email String @unique
|
|
38
|
+
hashedPassword String
|
|
39
|
+
salt String
|
|
40
|
+
resetToken String?
|
|
41
|
+
resetTokenExpiresAt DateTime?
|
|
42
|
+
webAuthnChallenge String? @unique
|
|
43
|
+
credentials UserCredential[]
|
|
44
|
+
createdAt DateTime @default(now())
|
|
45
|
+
updatedAt DateTime @updatedAt
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
model UserCredential {
|
|
49
|
+
id String @id
|
|
50
|
+
userId Int
|
|
51
|
+
user User @relation(fields: [userId], references: [id])
|
|
52
|
+
publicKey Bytes
|
|
53
|
+
transports String?
|
|
54
|
+
counter BigInt
|
|
55
|
+
}
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// any notes to print out when the job is done
|
|
61
|
+
const notes = exports.notes = [`${_cliHelpers.colors.warning('Done! But you have a little more work to do:')}\n`, 'You will need to add a couple of fields to your User table in order', 'to store a hashed password, salt, reset token, and to connect it to', 'a new UserCredential model to keep track of any devices used with', 'WebAuthn authentication:', '', ' model User {', ' id Int @id @default(autoincrement())', ' email String @unique', ' hashedPassword String', ' salt String', ' resetToken String?', ' resetTokenExpiresAt DateTime?', ' webAuthnChallenge String? @unique', ' credentials UserCredential[]', ' }', '', ' model UserCredential {', ' id String @id', ' userId Int', ' user User @relation(fields: [userId], references: [id])', ' publicKey Bytes', ' transports String?', ' counter BigInt', ' }', '', 'If you already have existing user records you will need to provide', 'a default value for `hashedPassword` and `salt` or Prisma complains, so', 'change those to: ', '', ' hashedPassword String @default("")', ' salt String @default("")', '', 'If you expose any of your user data via GraphQL be sure to exclude', '`hashedPassword` and `salt` (or whatever you named them) from the', 'SDL file that defines the fields for your user.', '', "You'll need to let Redwood know what fields you're using for your", "users' `id` and `username` fields. In this case we're using `id` and", '`email`, so update those in the `authFields` config in', `\`${_shared.functionsPath}/auth.js\`. This is also the place to tell Redwood if`, 'you used a different name for the `hashedPassword`, `salt`,', '`resetToken` or `resetTokenExpiresAt`, fields:`', '', ' authFields: {', " id: 'id',", " username: 'email',", " hashedPassword: 'hashedPassword',", " salt: 'salt',", " resetToken: 'resetToken',", " resetTokenExpiresAt: 'resetTokenExpiresAt',", " challenge: 'webAuthnChallenge'", ' },', '', "To get the actual user that's logged in, take a look at `getCurrentUser()`", `in \`${_shared.libPath}/auth.js\`. We default it to something simple, but you may`, 'use different names for your model or unique ID fields, in which case you', 'need to update those calls (instructions are in the comment above the code).', '', 'Finally, we created a SESSION_SECRET environment variable for you in', `${_path.default.join((0, _cliHelpers.getPaths)().base, '.env')}. This value should NOT be checked`, 'into version control and should be unique for each environment you', 'deploy to. If you ever need to log everyone out of your app at once', 'change this secret to a new value and deploy. To create a new secret, run:', '', ' yarn rw generate secret', ''];
|
|
62
|
+
const noteGenerate = exports.noteGenerate = ['', 'Need simple Login, Signup, Forgot Password pages and WebAuthn prompts?', "We've got a generator for those as well:", '', ' yarn rw generate dbAuth'];
|