@cedarjs/auth-dbauth-setup 7.0.0 → 8.0.0-canary.3265

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,57 @@
1
+ import type { AuthGeneratorCtx } from '@cedarjs/cli-helpers/auth/setupHelpers';
2
+ /**
3
+ * The OAuth providers `cedar setup auth dbAuth --oauth` knows how to
4
+ * configure out of the box. Anything else is served by implementing the
5
+ * `OAuthStrategy` interface, documented at
6
+ * https://cedarjs.com/docs/auth/dbauth#oauth-custom-strategies
7
+ */
8
+ export declare const KNOWN_OAUTH_PROVIDERS: readonly ["google", "github"];
9
+ export type OAuthProviderName = (typeof KNOWN_OAUTH_PROVIDERS)[number];
10
+ /**
11
+ * Parses the comma-separated value of `--oauth` into a deduped list of
12
+ * provider names, validating every entry against `KNOWN_OAUTH_PROVIDERS`.
13
+ *
14
+ * Returns an empty array when `oauth` is nullish (the flag wasn't passed).
15
+ * Some callers pass `null` explicitly for "flag absent"; others invoke the
16
+ * setup handler programmatically and simply omit the argument, which comes
17
+ * through as `undefined`. Both mean "OAuth disabled".
18
+ */
19
+ export declare function parseOAuthProviders(oauth: string | null | undefined): OAuthProviderName[];
20
+ export declare const apiPackages: string[];
21
+ export declare const createUserModelTask: {
22
+ title: string;
23
+ task: (ctx: AuthGeneratorCtx) => Promise<void>;
24
+ };
25
+ export declare const webAuthnCreateUserModelTask: {
26
+ title: string;
27
+ task: (ctx: AuthGeneratorCtx) => Promise<void>;
28
+ };
29
+ export declare function envVarNotes(providers: OAuthProviderName[]): string[];
30
+ export declare function notes(): string[];
31
+ export declare function webAuthnOauthNotes(): string[];
32
+ export declare const noteGenerate: string[];
33
+ /**
34
+ * Strips the config for every OAuth provider that wasn't selected out of the
35
+ * `auth.oauth.ts`/`auth.webAuthn.oauth.ts` templates, and drops the marker
36
+ * comments around the config for every provider that was.
37
+ *
38
+ * The templates carry every known provider so there's a single source of
39
+ * truth for the generated file's shape; this function is what turns that
40
+ * into a file that only configures the providers the user actually asked
41
+ * for. It's a pure string transform, so re-running setup (which always
42
+ * starts from a fresh copy of the template) produces byte-identical output.
43
+ *
44
+ * Exported for testing.
45
+ */
46
+ export declare function pruneOAuthProviders(content: string, providers: OAuthProviderName[]): string;
47
+ /**
48
+ * Rewrites the generated `functions/auth.ts` (or `.js`) in place so it only
49
+ * configures the OAuth providers the user selected with `--oauth`. Runs
50
+ * after `generateAuthApiFiles` has written the fresh, unpruned template, so
51
+ * every run starts from the same source and produces the same result.
52
+ */
53
+ export declare function configureOAuthProvidersTask(providers: OAuthProviderName[]): {
54
+ title: string;
55
+ task: () => void;
56
+ };
57
+ //# sourceMappingURL=oauth.setupData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.setupData.d.ts","sourceRoot":"","sources":["../src/oauth.setupData.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAA;AAO9E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,+BAAgC,CAAA;AAElE,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAMtE;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,iBAAiB,EAAE,CAqCrB;AAGD,eAAO,MAAM,WAAW,UAAsB,CAAA;AAmB9C,eAAO,MAAM,mBAAmB;;gBAEZ,gBAAgB;CAqBnC,CAAA;AAED,eAAO,MAAM,2BAA2B;;gBAEpB,gBAAgB;CAgCnC,CAAA;AAOD,wBAAgB,WAAW,CAAC,SAAS,EAAE,iBAAiB,EAAE,YAWzD;AAuCD,wBAAgB,KAAK,aAkDpB;AAED,wBAAgB,kBAAkB,aAgEjC;AAED,eAAO,MAAM,YAAY,UAMxB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,iBAAiB,EAAE,UA2B/B;AAMD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,iBAAiB,EAAE;;;EAsBzE"}
@@ -0,0 +1,336 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { colors } from "@cedarjs/cli-helpers/colors";
4
+ import { getPaths } from "@cedarjs/cli-helpers/paths";
5
+ import { isTypeScriptProject } from "@cedarjs/cli-helpers/project";
6
+ import { addModels, functionsPath, hasModel, libPath } from "./shared.js";
7
+ const KNOWN_OAUTH_PROVIDERS = ["google", "github"];
8
+ function isKnownOAuthProvider(value) {
9
+ return KNOWN_OAUTH_PROVIDERS.includes(value);
10
+ }
11
+ function parseOAuthProviders(oauth) {
12
+ if (oauth === null || oauth === void 0) {
13
+ return [];
14
+ }
15
+ const requested = oauth.split(",").map((provider) => provider.trim().toLowerCase()).filter(Boolean);
16
+ const uniqueRequested = [...new Set(requested)];
17
+ if (uniqueRequested.length === 0) {
18
+ throw new Error(
19
+ "--oauth requires at least one provider, e.g. --oauth google,github"
20
+ );
21
+ }
22
+ const unknownProviders = uniqueRequested.filter(
23
+ (provider) => !isKnownOAuthProvider(provider)
24
+ );
25
+ if (unknownProviders.length > 0) {
26
+ throw new Error(
27
+ `Unknown OAuth provider(s): ${unknownProviders.join(", ")}. Cedar ships built-in support for ${KNOWN_OAUTH_PROVIDERS.join(
28
+ ", "
29
+ )}. For any other provider, implement the \`OAuthStrategy\` interface from \`@cedarjs/auth-dbauth-oauth\` -- see https://cedarjs.com/docs/auth/dbauth#oauth-custom-strategies`
30
+ );
31
+ }
32
+ return uniqueRequested;
33
+ }
34
+ const apiPackages = ["oauth4webapi@^3"];
35
+ const OAUTH_MODEL = `
36
+ model OAuth {
37
+ id String @id @default(uuid())
38
+ provider String
39
+ providerUserId String
40
+ providerUsername String?
41
+ providerEmail String?
42
+ userId String
43
+ user User @relation(fields: [userId], references: [id])
44
+ createdAt DateTime @default(now())
45
+ updatedAt DateTime @updatedAt
46
+
47
+ @@unique([provider, providerUserId])
48
+ @@unique([userId, provider])
49
+ }
50
+ `;
51
+ const createUserModelTask = {
52
+ title: "Creating model `User`...",
53
+ task: async (ctx) => {
54
+ const hasUserModel = await hasModel("User");
55
+ if (hasUserModel && !ctx.force) {
56
+ throw new Error("User model already exists");
57
+ }
58
+ addModels(`
59
+ model User {
60
+ id String @id @default(uuid())
61
+ email String @unique
62
+ hashedPassword String?
63
+ salt String?
64
+ resetToken String?
65
+ resetTokenExpiresAt DateTime?
66
+ oauthIdentities OAuth[]
67
+ createdAt DateTime @default(now())
68
+ updatedAt DateTime @updatedAt
69
+ }
70
+ ${OAUTH_MODEL}`);
71
+ }
72
+ };
73
+ const webAuthnCreateUserModelTask = {
74
+ title: "Creating model `User`...",
75
+ task: async (ctx) => {
76
+ const hasUserModel = await hasModel("User");
77
+ if (hasUserModel && !ctx.force) {
78
+ throw new Error("User model already exists");
79
+ }
80
+ addModels(`
81
+ model User {
82
+ id String @id @default(uuid())
83
+ email String @unique
84
+ hashedPassword String?
85
+ salt String?
86
+ resetToken String?
87
+ resetTokenExpiresAt DateTime?
88
+ webAuthnChallenge String? @unique
89
+ credentials UserCredential[]
90
+ oauthIdentities OAuth[]
91
+ createdAt DateTime @default(now())
92
+ updatedAt DateTime @updatedAt
93
+ }
94
+
95
+ model UserCredential {
96
+ id String @id
97
+ userId String
98
+ user User @relation(fields: [userId], references: [id])
99
+ publicKey Bytes
100
+ transports String?
101
+ counter BigInt
102
+ }
103
+ ${OAUTH_MODEL}`);
104
+ }
105
+ };
106
+ const OAUTH_ENV_VAR_NAMES = {
107
+ google: ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"],
108
+ github: ["GITHUB_CLIENT_ID", "GITHUB_CLIENT_SECRET"]
109
+ };
110
+ function envVarNotes(providers) {
111
+ return [
112
+ "",
113
+ "Add the client id and secret for each configured OAuth provider to your .env file (get these from each provider\u2019s developer console):",
114
+ "",
115
+ ...providers.flatMap((provider) => [
116
+ ` # ${provider}`,
117
+ ...OAUTH_ENV_VAR_NAMES[provider].map((name) => ` ${name}=`)
118
+ ])
119
+ ];
120
+ }
121
+ function schemaNotes() {
122
+ return [
123
+ "",
124
+ "You will also need to add an `OAuth` model to your Prisma schema to",
125
+ "keep track of linked provider accounts:",
126
+ "",
127
+ " model OAuth {",
128
+ " id String @id @default(uuid())",
129
+ " provider String",
130
+ " providerUserId String",
131
+ " providerUsername String?",
132
+ " providerEmail String?",
133
+ " userId String",
134
+ " user User @relation(fields: [userId], references: [id])",
135
+ " createdAt DateTime @default(now())",
136
+ " updatedAt DateTime @updatedAt",
137
+ "",
138
+ " @@unique([provider, providerUserId])",
139
+ " @@unique([userId, provider])",
140
+ " }",
141
+ "",
142
+ "Since users can now sign up with just an OAuth provider (no password),",
143
+ "`hashedPassword` and `salt` on your `User` model must be optional (or",
144
+ "have a default value):",
145
+ "",
146
+ " hashedPassword String?",
147
+ " salt String?",
148
+ "",
149
+ "Don't forget to add the inverse relation to `User`:",
150
+ "",
151
+ " oauthIdentities OAuth[]"
152
+ ];
153
+ }
154
+ function notes() {
155
+ return [
156
+ `${colors.warning("Done! But you have a little more work to do:")}
157
+ `,
158
+ "You will need to add a couple of fields to your User table in order",
159
+ "to store a hashed password and salt:",
160
+ "",
161
+ " model User {",
162
+ " id String @id @default(uuid())",
163
+ " email String @unique",
164
+ " hashedPassword String? // <\u2500\u2510",
165
+ " salt String? // <\u2500\u2534\u2500 add these lines",
166
+ " resetToken String? // <\u2500\u2524",
167
+ " resetTokenExpiresAt DateTime? // <\u2500\u2518",
168
+ " }",
169
+ ...schemaNotes(),
170
+ "",
171
+ "If you expose any of your user data via GraphQL be sure to exclude",
172
+ "`hashedPassword` and `salt` (or whatever you named them) from the",
173
+ "SDL file that defines the fields for your user.",
174
+ "",
175
+ "You'll need to let Cedar know what fields you're using for your users'",
176
+ "`id` and `username` fields. In this case we're using `id` and `email`,",
177
+ "so update those in the `authFields` config in",
178
+ `\`${functionsPath}/auth.js\`. This is also the place to tell Cedar if`,
179
+ "you used a different name for the `hashedPassword`, `salt`,",
180
+ "`resetToken` or `resetTokenExpiresAt`, fields:`",
181
+ "",
182
+ " authFields: {",
183
+ " id: 'id',",
184
+ " username: 'email',",
185
+ " hashedPassword: 'hashedPassword',",
186
+ " salt: 'salt',",
187
+ " resetToken: 'resetToken',",
188
+ " resetTokenExpiresAt: 'resetTokenExpiresAt',",
189
+ " },",
190
+ "",
191
+ "To get the actual user that's logged in, take a look at `getCurrentUser()`",
192
+ `in \`${libPath}/auth.js\`. We default it to something simple, but you may`,
193
+ "use different names for your model or unique ID fields, in which case you",
194
+ "need to update those calls (instructions are in the comment above the code).",
195
+ "",
196
+ "Finally, we created a SESSION_SECRET environment variable for you in",
197
+ `${path.join(getPaths().base, ".env")}. This value should NOT be checked`,
198
+ "into version control and should be unique for each environment you",
199
+ "deploy to. If you ever need to log everyone out of your app at once",
200
+ "change this secret to a new value and deploy. To create a new secret, run:",
201
+ "",
202
+ " yarn cedar generate secret",
203
+ ""
204
+ ];
205
+ }
206
+ function webAuthnOauthNotes() {
207
+ return [
208
+ `${colors.warning("Done! But you have a little more work to do:")}
209
+ `,
210
+ "You will need to add a couple of fields to your User table in order",
211
+ "to store a hashed password, salt, reset token, and to connect it to",
212
+ "a new UserCredential model to keep track of any devices used with",
213
+ "WebAuthn authentication:",
214
+ "",
215
+ " model User {",
216
+ " id String @id @default(uuid())",
217
+ " email String @unique",
218
+ " hashedPassword String?",
219
+ " salt String?",
220
+ " resetToken String?",
221
+ " resetTokenExpiresAt DateTime?",
222
+ " webAuthnChallenge String? @unique",
223
+ " credentials UserCredential[]",
224
+ " }",
225
+ "",
226
+ " model UserCredential {",
227
+ " id String @id",
228
+ " userId String",
229
+ " user User @relation(fields: [userId], references: [id])",
230
+ " publicKey Bytes",
231
+ " transports String?",
232
+ " counter BigInt",
233
+ " }",
234
+ ...schemaNotes(),
235
+ "",
236
+ "If you expose any of your user data via GraphQL be sure to exclude",
237
+ "`hashedPassword` and `salt` (or whatever you named them) from the",
238
+ "SDL file that defines the fields for your user.",
239
+ "",
240
+ "You'll need to let Cedar know what fields you're using for your users'",
241
+ "`id` and `username` fields. In this case we're using `id` and `email`,",
242
+ "so update those in the `authFields` config in",
243
+ `\`${functionsPath}/auth.js\`. This is also the place to tell Cedar if`,
244
+ "you used a different name for the `hashedPassword`, `salt`,",
245
+ "`resetToken` or `resetTokenExpiresAt`, fields:`",
246
+ "",
247
+ " authFields: {",
248
+ " id: 'id',",
249
+ " username: 'email',",
250
+ " hashedPassword: 'hashedPassword',",
251
+ " salt: 'salt',",
252
+ " resetToken: 'resetToken',",
253
+ " resetTokenExpiresAt: 'resetTokenExpiresAt',",
254
+ " challenge: 'webAuthnChallenge'",
255
+ " },",
256
+ "",
257
+ "To get the actual user that's logged in, take a look at `getCurrentUser()`",
258
+ `in \`${libPath}/auth.js\`. We default it to something simple, but you may`,
259
+ "use different names for your model or unique ID fields, in which case you",
260
+ "need to update those calls (instructions are in the comment above the code).",
261
+ "",
262
+ "Finally, we created a SESSION_SECRET environment variable for you in",
263
+ `${path.join(getPaths().base, ".env")}. This value should NOT be checked`,
264
+ "into version control and should be unique for each environment you",
265
+ "deploy to. If you ever need to log everyone out of your app at once",
266
+ "change this secret to a new value and deploy. To create a new secret, run:",
267
+ "",
268
+ " yarn cedar generate secret",
269
+ ""
270
+ ];
271
+ }
272
+ const noteGenerate = [
273
+ "",
274
+ "Need simple Login, Signup and Forgot Password pages? We've got a generator",
275
+ "for those as well:",
276
+ "",
277
+ " yarn cedar generate dbAuth"
278
+ ];
279
+ function pruneOAuthProviders(content, providers) {
280
+ let result = content;
281
+ for (const provider of KNOWN_OAUTH_PROVIDERS) {
282
+ const startMarker = `// @oauth-provider:${provider}`;
283
+ const endMarker = `// @oauth-provider:${provider}:end`;
284
+ const blockPattern = new RegExp(
285
+ `[ \\t]*${escapeRegExp(startMarker)}\\n([\\s\\S]*?)[ \\t]*${escapeRegExp(
286
+ endMarker
287
+ )}\\n`
288
+ );
289
+ if (providers.includes(provider)) {
290
+ result = result.replace(blockPattern, "$1");
291
+ } else {
292
+ result = result.replace(blockPattern, "");
293
+ result = result.replace(
294
+ new RegExp(`^[ \\t]*${provider}Provider,\\n`, "m"),
295
+ ""
296
+ );
297
+ }
298
+ }
299
+ return result;
300
+ }
301
+ function escapeRegExp(str) {
302
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303
+ }
304
+ function configureOAuthProvidersTask(providers) {
305
+ return {
306
+ title: "Configuring selected OAuth providers...",
307
+ task: () => {
308
+ const ext = isTypeScriptProject() ? "ts" : "js";
309
+ const authFunctionPath = path.join(
310
+ getPaths().api.functions,
311
+ `auth.${ext}`
312
+ );
313
+ if (!fs.existsSync(authFunctionPath)) {
314
+ return;
315
+ }
316
+ const content = fs.readFileSync(authFunctionPath, "utf-8");
317
+ const prunedContent = pruneOAuthProviders(content, providers);
318
+ if (prunedContent !== content) {
319
+ fs.writeFileSync(authFunctionPath, prunedContent);
320
+ }
321
+ }
322
+ };
323
+ }
324
+ export {
325
+ KNOWN_OAUTH_PROVIDERS,
326
+ apiPackages,
327
+ configureOAuthProvidersTask,
328
+ createUserModelTask,
329
+ envVarNotes,
330
+ noteGenerate,
331
+ notes,
332
+ parseOAuthProviders,
333
+ pruneOAuthProviders,
334
+ webAuthnCreateUserModelTask,
335
+ webAuthnOauthNotes
336
+ };
package/dist/setup.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare const description = "Set up auth for for dbAuth";
4
4
  export declare function builder(yargs: Argv): void;
5
5
  export interface Args {
6
6
  webauthn: boolean | null;
7
+ oauth?: string | null;
7
8
  createUserModel: boolean | null;
8
9
  generateAuthPages: boolean | null;
9
10
  force: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEjC,eAAO,MAAM,OAAO,WAAW,CAAA;AAC/B,eAAO,MAAM,WAAW,+BAA+B,CAAA;AAEvD,wBAAgB,OAAO,CAAC,KAAK,EAAE,IAAI,QAgClC;AAED,MAAM,WAAW,IAAI;IACnB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAA;IACxB,eAAe,EAAE,OAAO,GAAG,IAAI,CAAA;IAC/B,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,KAAK,EAAE,OAAO,CAAA;CACf;AAED,eAAO,MAAM,OAAO,GAAU,SAAS,IAAI,kBAG1C,CAAA"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAEjC,eAAO,MAAM,OAAO,WAAW,CAAA;AAC/B,eAAO,MAAM,WAAW,+BAA+B,CAAA;AAEvD,wBAAgB,OAAO,CAAC,KAAK,EAAE,IAAI,QAwClC;AAED,MAAM,WAAW,IAAI;IACnB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAA;IAKxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,eAAe,EAAE,OAAO,GAAG,IAAI,CAAA;IAC/B,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,KAAK,EAAE,OAAO,CAAA;CACf;AAED,eAAO,MAAM,OAAO,GAAU,SAAS,IAAI,kBAG1C,CAAA"}
package/dist/setup.js CHANGED
@@ -12,6 +12,11 @@ function builder(yargs) {
12
12
  default: null,
13
13
  description: "Include WebAuthn support (TouchID/FaceID)",
14
14
  type: "boolean"
15
+ }).option("oauth", {
16
+ alias: "o",
17
+ default: null,
18
+ description: "Include OAuth support for the given comma-separated providers (google, github), e.g. --oauth google,github",
19
+ type: "string"
15
20
  }).option("createUserModel", {
16
21
  alias: "u",
17
22
  default: null,
@@ -1,4 +1,4 @@
1
- import type { AuthGeneratorCtx } from '@cedarjs/cli-helpers';
1
+ import type { AuthGeneratorCtx } from '@cedarjs/cli-helpers/auth/setupHelpers';
2
2
  export declare const extraTask: {
3
3
  title: string;
4
4
  task: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"setupData.d.ts","sourceRoot":"","sources":["../src/setupData.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAM5D,eAAO,MAAM,SAAS;;;CAKrB,CAAA;AAED,eAAO,MAAM,mBAAmB;;gBAEZ,gBAAgB;CAoBnC,CAAA;AAGD,eAAO,MAAM,KAAK,UAsDjB,CAAA;AAED,eAAO,MAAM,qBAAqB,UAwBjC,CAAA;AAED,eAAO,MAAM,YAAY,UAMxB,CAAA;AAED,eAAO,MAAM,cAAc,UAO1B,CAAA"}
1
+ {"version":3,"file":"setupData.d.ts","sourceRoot":"","sources":["../src/setupData.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAA;AAS9E,eAAO,MAAM,SAAS;;;CAKrB,CAAA;AAED,eAAO,MAAM,mBAAmB;;gBAEZ,gBAAgB;CAoBnC,CAAA;AAGD,eAAO,MAAM,KAAK,UAsDjB,CAAA;AAED,eAAO,MAAM,qBAAqB,UAwBjC,CAAA;AAED,eAAO,MAAM,YAAY,UAMxB,CAAA;AAED,eAAO,MAAM,cAAc,UAO1B,CAAA"}
package/dist/setupData.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
- import { getPaths, colors, addEnvVarTask } from "@cedarjs/cli-helpers";
3
+ import { colors } from "@cedarjs/cli-helpers/colors";
4
+ import { getPaths } from "@cedarjs/cli-helpers/paths";
5
+ import { addEnvVarTask } from "@cedarjs/cli-helpers/project";
4
6
  import { addModels, functionsPath, hasModel, libPath } from "./shared.js";
5
7
  const secret = crypto.randomBytes(32).toString("base64");
6
8
  const extraTask = addEnvVarTask(
@@ -1,5 +1,5 @@
1
1
  import type { Args } from './setup.js';
2
- export declare function handler({ webauthn, createUserModel, generateAuthPages, force: forceArg, }: Args): Promise<void>;
2
+ export declare function handler({ webauthn, oauth, createUserModel, generateAuthPages, force: forceArg, }: Args): Promise<void>;
3
3
  export declare const createAuthDecoderFunction: {
4
4
  title: string;
5
5
  task: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"setupHandler.d.ts","sourceRoot":"","sources":["../src/setupHandler.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAwBtC,wBAAsB,OAAO,CAAC,EAC5B,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,KAAK,EAAE,QAAQ,GAChB,EAAE,IAAI,iBAyEN;AAsED,eAAO,MAAM,yBAAyB;;;CAuCrC,CAAA"}
1
+ {"version":3,"file":"setupHandler.d.ts","sourceRoot":"","sources":["../src/setupHandler.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAwBtC,wBAAsB,OAAO,CAAC,EAC5B,QAAQ,EACR,KAAK,EACL,eAAe,EACf,iBAAiB,EACjB,KAAK,EAAE,QAAQ,GAChB,EAAE,IAAI,iBA0GN;AA4ED,eAAO,MAAM,yBAAyB;;;CAuCrC,CAAA"}
@@ -1,7 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import prompts from "prompts";
4
- import { getGraphqlPath, standardAuthHandler } from "@cedarjs/cli-helpers";
4
+ import { standardAuthHandler } from "@cedarjs/cli-helpers/auth/setupHelpers";
5
+ import { getGraphqlPath } from "@cedarjs/cli-helpers/project";
6
+ import { requireTTYOrExit } from "@cedarjs/cli-helpers/tty";
7
+ import {
8
+ apiPackages as oauthApiPackages,
9
+ configureOAuthProvidersTask,
10
+ createUserModelTask as oauthCreateUserModelTask,
11
+ envVarNotes as oauthEnvVarNotes,
12
+ noteGenerate as oauthNoteGenerate,
13
+ notes as oauthNotes,
14
+ parseOAuthProviders,
15
+ webAuthnCreateUserModelTask as webAuthnOauthCreateUserModelTask,
16
+ webAuthnOauthNotes
17
+ } from "./oauth.setupData.js";
5
18
  import {
6
19
  notes,
7
20
  noteGenerate,
@@ -26,6 +39,7 @@ import {
26
39
  } from "./webAuthn.setupData.js";
27
40
  async function handler({
28
41
  webauthn,
42
+ oauth,
29
43
  createUserModel,
30
44
  generateAuthPages,
31
45
  force: forceArg
@@ -37,10 +51,22 @@ async function handler({
37
51
  )
38
52
  );
39
53
  const webAuthn = await shouldIncludeWebAuthn(webauthn);
54
+ const oauthProviders = parseOAuthProviders(oauth);
55
+ const includeOAuth = oauthProviders.length > 0;
40
56
  const createDbUserModel = await shouldCreateUserModel(createUserModel);
41
57
  const generateDbAuthPages = await shouldGenerateDbAuthPages(generateAuthPages);
42
58
  const oneMoreThing = [];
43
- if (webAuthn) {
59
+ if (webAuthn && includeOAuth) {
60
+ if (createDbUserModel) {
61
+ oneMoreThing.push(...notesCreatedUserModel);
62
+ } else {
63
+ oneMoreThing.push(...webAuthnOauthNotes());
64
+ }
65
+ oneMoreThing.push(...oauthEnvVarNotes(oauthProviders));
66
+ if (!generateDbAuthPages) {
67
+ oneMoreThing.push(...webAuthnNoteGenerate);
68
+ }
69
+ } else if (webAuthn) {
44
70
  if (createDbUserModel) {
45
71
  oneMoreThing.push(...notesCreatedUserModel);
46
72
  } else {
@@ -49,6 +75,16 @@ async function handler({
49
75
  if (!generateDbAuthPages) {
50
76
  oneMoreThing.push(...webAuthnNoteGenerate);
51
77
  }
78
+ } else if (includeOAuth) {
79
+ if (createDbUserModel) {
80
+ oneMoreThing.push(...notesCreatedUserModel);
81
+ } else {
82
+ oneMoreThing.push(...oauthNotes());
83
+ }
84
+ oneMoreThing.push(...oauthEnvVarNotes(oauthProviders));
85
+ if (!generateDbAuthPages) {
86
+ oneMoreThing.push(...oauthNoteGenerate);
87
+ }
52
88
  } else {
53
89
  if (createDbUserModel) {
54
90
  oneMoreThing.push(...notesCreatedUserModel);
@@ -62,8 +98,12 @@ async function handler({
62
98
  oneMoreThing.push(...notePrivateSet);
63
99
  let createDbUserModelTask = void 0;
64
100
  if (createDbUserModel) {
65
- if (webAuthn) {
101
+ if (webAuthn && includeOAuth) {
102
+ createDbUserModelTask = webAuthnOauthCreateUserModelTask;
103
+ } else if (webAuthn) {
66
104
  createDbUserModelTask = webAuthnCreateUserModelTask;
105
+ } else if (includeOAuth) {
106
+ createDbUserModelTask = oauthCreateUserModelTask;
67
107
  } else {
68
108
  createDbUserModelTask = createUserModelTask;
69
109
  }
@@ -74,18 +114,21 @@ async function handler({
74
114
  provider: "dbAuth",
75
115
  authDecoderImport: "import { createAuthDecoder } from '@cedarjs/auth-dbauth-api'",
76
116
  webAuthn,
117
+ oauth: includeOAuth,
77
118
  webPackages: [
78
119
  `@cedarjs/auth-dbauth-web@${version}`,
79
120
  ...webAuthn ? webAuthnWebPackages : []
80
121
  ],
81
122
  apiPackages: [
82
123
  `@cedarjs/auth-dbauth-api@${version}`,
83
- ...webAuthn ? webAuthnApiPackages : []
124
+ ...webAuthn ? webAuthnApiPackages : [],
125
+ ...includeOAuth ? [`@cedarjs/auth-dbauth-oauth@${version}`, ...oauthApiPackages] : []
84
126
  ],
85
127
  extraTasks: [
86
128
  webAuthn ? webAuthnExtraTask : extraTask,
87
129
  createDbUserModelTask,
88
130
  createAuthDecoderFunction,
131
+ includeOAuth ? configureOAuthProvidersTask(oauthProviders) : void 0,
89
132
  generateDbAuthPages ? generateAuthPagesTask(createDbUserModel) : void 0
90
133
  ],
91
134
  notes: oneMoreThing
@@ -93,6 +136,7 @@ async function handler({
93
136
  }
94
137
  async function shouldIncludeWebAuthn(webauthn) {
95
138
  if (webauthn === null) {
139
+ requireTTYOrExit("--webauthn or --webauthn=false");
96
140
  const webAuthnResponse = await prompts({
97
141
  type: "confirm",
98
142
  name: "answer",
@@ -111,6 +155,7 @@ async function shouldCreateUserModel(createUserModel) {
111
155
  return true;
112
156
  }
113
157
  if (createUserModel === null && !hasUserModel) {
158
+ requireTTYOrExit("--createUserModel or --createUserModel=false");
114
159
  const createModelResponse = await prompts({
115
160
  type: "confirm",
116
161
  name: "answer",
@@ -123,6 +168,7 @@ async function shouldCreateUserModel(createUserModel) {
123
168
  }
124
169
  async function shouldGenerateDbAuthPages(generateAuthPages) {
125
170
  if (generateAuthPages === null && !hasAuthPages()) {
171
+ requireTTYOrExit("--generateAuthPages or --generateAuthPages=false");
126
172
  const generateAuthPagesResponse = await prompts({
127
173
  type: "confirm",
128
174
  name: "answer",
package/dist/shared.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import prismaInternals from "@prisma/internals";
4
- import { getPaths } from "@cedarjs/cli-helpers";
5
4
  import { runBin } from "@cedarjs/cli-helpers/packageManager/exec";
5
+ import { getPaths } from "@cedarjs/cli-helpers/paths";
6
6
  import {
7
7
  processPagesDir,
8
8
  getSchemaPath,
@@ -0,0 +1,327 @@
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
+ interface UserAttributes {
235
+ name: string
236
+ }
237
+
238
+ const signupOptions: DbAuthHandlerOptions<
239
+ UserType,
240
+ UserAttributes
241
+ >['signup'] = {
242
+ // Whatever you want to happen to your data on new user signup. Cedar will
243
+ // check for duplicate usernames before calling this handler. At a minimum
244
+ // you need to save the `username`, `hashedPassword` and `salt` to your
245
+ // user table. `userAttributes` contains any additional object members that
246
+ // were included in the object given to the `signUp()` function you got
247
+ // from `useAuth()`.
248
+ //
249
+ // If you want the user to be immediately logged in, return the user that
250
+ // was created.
251
+ //
252
+ // If this handler throws an error, it will be returned by the `signUp()`
253
+ // function in the form of: `{ error: 'Error message' }`.
254
+ //
255
+ // If this returns anything else, it will be returned by the
256
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
257
+ handler: ({
258
+ username,
259
+ hashedPassword,
260
+ salt,
261
+ userAttributes: _userAttributes,
262
+ }) => {
263
+ return db.user.create({
264
+ data: {
265
+ email: username,
266
+ hashedPassword: hashedPassword,
267
+ salt: salt,
268
+ // name: userAttributes.name
269
+ },
270
+ })
271
+ },
272
+
273
+ // Include any format checks for password here. Return `true` if the
274
+ // password is valid, otherwise throw a `PasswordValidationError`.
275
+ // Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
276
+ passwordValidation: (_password) => {
277
+ return true
278
+ },
279
+
280
+ errors: {
281
+ // `field` will be either "username" or "password"
282
+ fieldMissing: '${field} is required',
283
+ usernameTaken: 'Username `${username}` already in use',
284
+ },
285
+ }
286
+
287
+ const authHandler = new DbAuthHandler(event, context, {
288
+ // Provide prisma db client
289
+ db: db,
290
+
291
+ // The name of the property you'd call on `db` to access your user table.
292
+ // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user`
293
+ authModelAccessor: 'user',
294
+
295
+ // A map of what dbAuth calls a field to what your database calls it.
296
+ // `id` is whatever column you use to uniquely identify a user (probably
297
+ // something like `id` or `userId` or even `email`)
298
+ authFields: {
299
+ id: 'id',
300
+ username: 'email',
301
+ hashedPassword: 'hashedPassword',
302
+ salt: 'salt',
303
+ resetToken: 'resetToken',
304
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
305
+ },
306
+
307
+ // A list of fields on your user object that are safe to return to the
308
+ // client when invoking a handler that returns a user (like forgotPassword
309
+ // and signup). This list should be as small as possible to be sure not to
310
+ // leak any sensitive information to the client.
311
+ allowedUserFields: ['id', 'email'],
312
+
313
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
314
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
315
+ cookie: {
316
+ attributes: cookieAttributes,
317
+ name: cookieName,
318
+ },
319
+
320
+ forgotPassword: forgotPasswordOptions,
321
+ login: loginOptions,
322
+ resetPassword: resetPasswordOptions,
323
+ signup: signupOptions,
324
+ })
325
+
326
+ return await authHandler.invoke()
327
+ }
@@ -0,0 +1,345 @@
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. With the default dbAuth routes and field names, the
146
+ // URL to reset the password will be:
147
+ //
148
+ // https://example.com/reset-password?resetToken=${user.resetToken}
149
+ //
150
+ // Whatever is returned from this function will be returned from
151
+ // the `forgotPassword()` function that is destructured from `useAuth()`
152
+ // You could use this return value to, for example, show the email
153
+ // address in a toast message so the user will know it worked and where
154
+ // to look for the email.
155
+ handler: (user) => {
156
+ return user
157
+ },
158
+
159
+ // How long the resetToken is valid for, in seconds (default is 24 hours)
160
+ expires: 60 * 60 * 24,
161
+
162
+ errors: {
163
+ // for security reasons you may want to be vague here rather than expose
164
+ // the fact that the email address wasn't found (prevents fishing for
165
+ // valid email addresses)
166
+ usernameNotFound: 'Username not found',
167
+ // if the user somehow gets around client validation
168
+ usernameRequired: 'Username is required',
169
+ },
170
+ }
171
+
172
+ const loginOptions: DbAuthHandlerOptions['login'] = {
173
+ // handler() is called after finding the user that matches the
174
+ // username/password provided at login, but before actually considering them
175
+ // logged in. The `user` argument will be the user in the database that
176
+ // matched the username/password.
177
+ //
178
+ // If you want to allow this user to log in simply return the user.
179
+ //
180
+ // If you want to prevent someone logging in for another reason (maybe they
181
+ // didn't validate their email yet), throw an error and it will be returned
182
+ // by the `logIn()` function from `useAuth()` in the form of:
183
+ // `{ message: 'Error message' }`
184
+ handler: (user) => {
185
+ return user
186
+ },
187
+
188
+ errors: {
189
+ usernameOrPasswordMissing: 'Both username and password are required',
190
+ usernameNotFound: 'Username ${username} not found',
191
+ // For security reasons you may want to make this the same as the
192
+ // usernameNotFound error so that a malicious user can't use the error
193
+ // to narrow down if it's the username or password that's incorrect
194
+ incorrectPassword: 'Incorrect password for ${username}',
195
+ },
196
+
197
+ // How long a user will remain logged in, in seconds
198
+ expires: 60 * 60 * 24 * 365 * 10,
199
+ }
200
+
201
+ const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = {
202
+ // handler() is invoked after the password has been successfully updated in
203
+ // the database. Returning anything truthy will automatically logs the user
204
+ // in. Return `false` otherwise, and in the Reset Password page redirect the
205
+ // user to the login page.
206
+ handler: (_user) => {
207
+ return true
208
+ },
209
+
210
+ // If `false` then the new password MUST be different than the current one
211
+ allowReusedPassword: true,
212
+
213
+ errors: {
214
+ // the resetToken is valid, but expired
215
+ resetTokenExpired: 'resetToken is expired',
216
+ // no user was found with the given resetToken
217
+ resetTokenInvalid: 'resetToken is invalid',
218
+ // the resetToken was not present in the URL
219
+ resetTokenRequired: 'resetToken is required',
220
+ // new password is the same as the old password (apparently they did not forget it)
221
+ reusedPassword: 'Must choose a new password',
222
+ },
223
+ }
224
+
225
+ interface UserAttributes {
226
+ name: string
227
+ }
228
+
229
+ const signupOptions: DbAuthHandlerOptions<
230
+ UserType,
231
+ UserAttributes
232
+ >['signup'] = {
233
+ // Whatever you want to happen to your data on new user signup. Cedar will
234
+ // check for duplicate usernames before calling this handler. At a minimum
235
+ // you need to save the `username`, `hashedPassword` and `salt` to your
236
+ // user table. `userAttributes` contains any additional object members that
237
+ // were included in the object given to the `signUp()` function you got
238
+ // from `useAuth()`.
239
+ //
240
+ // If you want the user to be immediately logged in, return the user that
241
+ // was created.
242
+ //
243
+ // If this handler throws an error, it will be returned by the `signUp()`
244
+ // function in the form of: `{ error: 'Error message' }`.
245
+ //
246
+ // If this returns anything else, it will be returned by the
247
+ // `signUp()` function in the form of: `{ message: 'String here' }`.
248
+ handler: ({
249
+ username,
250
+ hashedPassword,
251
+ salt,
252
+ userAttributes: _userAttributes,
253
+ }) => {
254
+ return db.user.create({
255
+ data: {
256
+ email: username,
257
+ hashedPassword: hashedPassword,
258
+ salt: salt,
259
+ // name: userAttributes.name
260
+ },
261
+ })
262
+ },
263
+
264
+ // Include any format checks for password here. Return `true` if the
265
+ // password is valid, otherwise throw a `PasswordValidationError`.
266
+ // Import the error along with `DbAuthHandler` from `@cedarjs/api` above.
267
+ passwordValidation: (_password) => {
268
+ return true
269
+ },
270
+
271
+ errors: {
272
+ // `field` will be either "username" or "password"
273
+ fieldMissing: '${field} is required',
274
+ usernameTaken: 'Username `${username}` already in use',
275
+ },
276
+ }
277
+
278
+ const authHandler = new DbAuthHandler(event, context, {
279
+ // Provide prisma db client
280
+ db: db,
281
+
282
+ // The name of the property you'd call on `db` to access your user table.
283
+ // ie. if your Prisma model is named `User` this value would be `user`, as in `db.user`
284
+ authModelAccessor: 'user',
285
+
286
+ // The name of the property you'd call on `db` to access your user credentials table.
287
+ // ie. if your Prisma model is named `UserCredential` this value would be `userCredential`, as in `db.userCredential`
288
+ credentialModelAccessor: 'userCredential',
289
+
290
+ // A map of what dbAuth calls a field to what your database calls it.
291
+ // `id` is whatever column you use to uniquely identify a user (probably
292
+ // something like `id` or `userId` or even `email`)
293
+ authFields: {
294
+ id: 'id',
295
+ username: 'email',
296
+ hashedPassword: 'hashedPassword',
297
+ salt: 'salt',
298
+ resetToken: 'resetToken',
299
+ resetTokenExpiresAt: 'resetTokenExpiresAt',
300
+ challenge: 'webAuthnChallenge',
301
+ },
302
+
303
+ // Specifies attributes on the cookie that dbAuth sets in order to remember
304
+ // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
305
+ cookie: {
306
+ attributes: cookieAttributes,
307
+ name: cookieName,
308
+ },
309
+
310
+ forgotPassword: forgotPasswordOptions,
311
+ login: loginOptions,
312
+ resetPassword: resetPasswordOptions,
313
+ signup: signupOptions,
314
+
315
+ // See https://cedarjs.com/docs/auth/dbauth#webauthn for options
316
+ webAuthn: {
317
+ enabled: true,
318
+ // How long to allow re-auth via WebAuthn in seconds (default is 10 years).
319
+ // The `login.expires` time denotes how many seconds before a user will be
320
+ // logged out, and this value is how long they'll be to continue to use a
321
+ // fingerprint/face scan to log in again. When this one expires they
322
+ // *must* re-enter username and password to authenticate (WebAuthn will
323
+ // then be re-enabled for this amount of time).
324
+ expires: 60 * 60 * 24 * 365 * 10,
325
+ name: 'Cedar Application',
326
+ domain:
327
+ process.env.NODE_ENV === 'development' ? 'localhost' : 'server.com',
328
+ origin:
329
+ process.env.NODE_ENV === 'development'
330
+ ? 'http://localhost:8910'
331
+ : 'https://server.com',
332
+ type: 'platform',
333
+ timeout: 60000,
334
+ credentialFields: {
335
+ id: 'id',
336
+ userId: 'userId',
337
+ publicKey: 'publicKey',
338
+ transports: 'transports',
339
+ counter: 'counter',
340
+ },
341
+ },
342
+ })
343
+
344
+ return await authHandler.invoke()
345
+ }
@@ -1,4 +1,4 @@
1
- import type { AuthGeneratorCtx } from '@cedarjs/cli-helpers';
1
+ import type { AuthGeneratorCtx } from '@cedarjs/cli-helpers/auth/setupHelpers';
2
2
  export { extraTask } from './setupData.js';
3
3
  export declare const webPackages: string[];
4
4
  export declare const apiPackages: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"webAuthn.setupData.d.ts","sourceRoot":"","sources":["../src/webAuthn.setupData.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAK5D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAG1C,eAAO,MAAM,WAAW,UAAkC,CAAA;AAG1D,eAAO,MAAM,WAAW,UAAiC,CAAA;AAEzD,eAAO,MAAM,mBAAmB;;gBAEZ,gBAAgB;CA+BnC,CAAA;AAGD,eAAO,MAAM,KAAK,UAoEjB,CAAA;AAED,eAAO,MAAM,YAAY,UAMxB,CAAA"}
1
+ {"version":3,"file":"webAuthn.setupData.d.ts","sourceRoot":"","sources":["../src/webAuthn.setupData.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wCAAwC,CAAA;AAO9E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAG1C,eAAO,MAAM,WAAW,UAAkC,CAAA;AAG1D,eAAO,MAAM,WAAW,UAAiC,CAAA;AAEzD,eAAO,MAAM,mBAAmB;;gBAEZ,gBAAgB;CA+BnC,CAAA;AAGD,eAAO,MAAM,KAAK,UAoEjB,CAAA;AAED,eAAO,MAAM,YAAY,UAMxB,CAAA"}
@@ -1,9 +1,10 @@
1
1
  import path from "node:path";
2
- import { getPaths, colors } from "@cedarjs/cli-helpers";
2
+ import { colors } from "@cedarjs/cli-helpers/colors";
3
+ import { getPaths } from "@cedarjs/cli-helpers/paths";
3
4
  import { addModels, functionsPath, hasModel, libPath } from "./shared.js";
4
5
  import { extraTask } from "./setupData.js";
5
- const webPackages = ["@simplewebauthn/browser@^10"];
6
- const apiPackages = ["@simplewebauthn/server@^10"];
6
+ const webPackages = ["@simplewebauthn/browser@^13"];
7
+ const apiPackages = ["@simplewebauthn/server@^13"];
7
8
  const createUserModelTask = {
8
9
  title: "Creating model `User`...",
9
10
  task: async (ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/auth-dbauth-setup",
3
- "version": "7.0.0",
3
+ "version": "8.0.0-canary.3265",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -23,16 +23,16 @@
23
23
  "test:watch": "yarn test --watch"
24
24
  },
25
25
  "dependencies": {
26
- "@cedarjs/cli-helpers": "7.0.0",
27
- "@cedarjs/project-config": "7.0.0",
28
- "@prisma/internals": "7.8.0",
26
+ "@cedarjs/cli-helpers": "8.0.0-canary.3265",
27
+ "@cedarjs/project-config": "8.0.0-canary.3265",
28
+ "@prisma/internals": "7.10.0",
29
29
  "prompts": "2.4.2",
30
30
  "termi-link": "1.1.0"
31
31
  },
32
32
  "devDependencies": {
33
- "@cedarjs/framework-tools": "7.0.0",
33
+ "@cedarjs/framework-tools": "8.0.0-canary.3265",
34
34
  "@types/yargs": "17.0.35",
35
- "memfs": "4.78.1",
35
+ "memfs": "4.79.0",
36
36
  "nodemon": "3.1.14",
37
37
  "ts-dedent": "2.3.0",
38
38
  "typescript": "5.9.3",