@warlock.js/auth 5.11.0 → 5.13.0
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/CHANGELOG.md +65 -0
- package/README.md +88 -86
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/contracts/index.d.mts +4 -2
- package/esm/contracts/providers.d.mts +101 -0
- package/esm/contracts/providers.d.mts.map +1 -0
- package/esm/contracts/request-locals.d.mts +43 -0
- package/esm/contracts/request-locals.d.mts.map +1 -0
- package/esm/contracts/types.d.mts +159 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
- package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
- package/esm/errors/email-not-verified.error.d.mts +16 -0
- package/esm/errors/email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/email-not-verified.error.mjs +20 -0
- package/esm/errors/email-not-verified.error.mjs.map +1 -0
- package/esm/errors/index.d.mts +7 -0
- package/esm/errors/index.mjs +9 -0
- package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
- package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
- package/esm/errors/invalid-one-time-token.error.mjs +21 -0
- package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
- package/esm/errors/invalid-passkey.error.d.mts +18 -0
- package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
- package/esm/errors/invalid-passkey.error.mjs +23 -0
- package/esm/errors/invalid-passkey.error.mjs.map +1 -0
- package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
- package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
- package/esm/errors/invalid-provider-callback.error.mjs +23 -0
- package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
- package/esm/errors/notifications-unavailable.error.d.mts +15 -0
- package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
- package/esm/errors/notifications-unavailable.error.mjs +20 -0
- package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
- package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
- package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/provider-email-not-verified.error.mjs +20 -0
- package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
- package/esm/index.d.mts +31 -3
- package/esm/index.mjs +29 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +13 -2
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/csrf-origin-check.d.mts +40 -0
- package/esm/middleware/csrf-origin-check.d.mts.map +1 -0
- package/esm/middleware/csrf-origin-check.mjs +105 -0
- package/esm/middleware/csrf-origin-check.mjs.map +1 -0
- package/esm/middleware/index.d.mts +5 -1
- package/esm/middleware/index.mjs +4 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
- package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/otp-throttle.middleware.mjs +45 -0
- package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
- package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
- package/esm/middleware/require-verified-email.middleware.mjs +24 -0
- package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
- package/esm/models/access-token/access-token.model.d.mts +8 -0
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +3 -0
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/index.d.mts +3 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +16 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/one-time-token/index.d.mts +1 -0
- package/esm/models/one-time-token/index.mjs +3 -0
- package/esm/models/one-time-token/migration.mjs +31 -0
- package/esm/models/one-time-token/migration.mjs.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
- package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
- package/esm/models/passkey-credential/index.d.mts +1 -0
- package/esm/models/passkey-credential/index.mjs +3 -0
- package/esm/models/passkey-credential/migration.mjs +25 -0
- package/esm/models/passkey-credential/migration.mjs.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
- package/esm/models/provider-account/index.d.mts +1 -0
- package/esm/models/provider-account/index.mjs +3 -0
- package/esm/models/provider-account/migration.mjs +25 -0
- package/esm/models/provider-account/migration.mjs.map +1 -0
- package/esm/models/provider-account/provider-account.model.d.mts +75 -0
- package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
- package/esm/models/provider-account/provider-account.model.mjs +55 -0
- package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/otp/index.d.mts +1 -0
- package/esm/otp/index.mjs +3 -0
- package/esm/otp/otp.d.mts +38 -0
- package/esm/otp/otp.d.mts.map +1 -0
- package/esm/otp/otp.mjs +105 -0
- package/esm/otp/otp.mjs.map +1 -0
- package/esm/passkeys/index.d.mts +3 -0
- package/esm/passkeys/index.mjs +4 -0
- package/esm/passkeys/passkey-authentication.d.mts +33 -0
- package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
- package/esm/passkeys/passkey-authentication.mjs +93 -0
- package/esm/passkeys/passkey-authentication.mjs.map +1 -0
- package/esm/passkeys/passkey-challenges.mjs +53 -0
- package/esm/passkeys/passkey-challenges.mjs.map +1 -0
- package/esm/passkeys/passkey-registration.d.mts +29 -0
- package/esm/passkeys/passkey-registration.d.mts.map +1 -0
- package/esm/passkeys/passkey-registration.mjs +88 -0
- package/esm/passkeys/passkey-registration.mjs.map +1 -0
- package/esm/passkeys/simplewebauthn.d.mts +21 -0
- package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
- package/esm/passkeys/simplewebauthn.mjs +13 -0
- package/esm/passkeys/simplewebauthn.mjs.map +1 -0
- package/esm/providers/google-provider.d.mts +40 -0
- package/esm/providers/google-provider.d.mts.map +1 -0
- package/esm/providers/google-provider.mjs +105 -0
- package/esm/providers/google-provider.mjs.map +1 -0
- package/esm/providers/index.d.mts +2 -0
- package/esm/providers/index.mjs +4 -0
- package/esm/providers/provider-login.d.mts +43 -0
- package/esm/providers/provider-login.d.mts.map +1 -0
- package/esm/providers/provider-login.mjs +86 -0
- package/esm/providers/provider-login.mjs.map +1 -0
- package/esm/providers/provider-state-cookie.mjs +63 -0
- package/esm/providers/provider-state-cookie.mjs.map +1 -0
- package/esm/providers/resolve-provider-user.mjs +45 -0
- package/esm/providers/resolve-provider-user.mjs.map +1 -0
- package/esm/services/auth-config.mjs +84 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +59 -2
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +86 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/current-user.d.mts +21 -0
- package/esm/services/current-user.d.mts.map +1 -0
- package/esm/services/current-user.mjs +24 -0
- package/esm/services/current-user.mjs.map +1 -0
- package/esm/services/email-verification.d.mts +35 -0
- package/esm/services/email-verification.d.mts.map +1 -0
- package/esm/services/email-verification.mjs +55 -0
- package/esm/services/email-verification.mjs.map +1 -0
- package/esm/services/index.d.mts +5 -1
- package/esm/services/index.mjs +4 -0
- package/esm/services/one-time-tokens.d.mts +32 -0
- package/esm/services/one-time-tokens.d.mts.map +1 -0
- package/esm/services/one-time-tokens.mjs +56 -0
- package/esm/services/one-time-tokens.mjs.map +1 -0
- package/esm/services/optional-peer.mjs +37 -0
- package/esm/services/optional-peer.mjs.map +1 -0
- package/esm/services/password-reset.d.mts +34 -0
- package/esm/services/password-reset.d.mts.map +1 -0
- package/esm/services/password-reset.mjs +81 -0
- package/esm/services/password-reset.mjs.map +1 -0
- package/esm/services/token-notifications.mjs +87 -0
- package/esm/services/token-notifications.mjs.map +1 -0
- package/esm/utils/auth-error-codes.d.mts +48 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +41 -0
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/esm/utils/index.mjs +1 -0
- package/esm/utils/safe-equal.d.mts +6 -0
- package/esm/utils/safe-equal.d.mts.map +1 -0
- package/esm/utils/safe-equal.mjs +13 -0
- package/esm/utils/safe-equal.mjs.map +1 -0
- package/llms-full.txt +402 -29
- package/llms.txt +2 -0
- package/package.json +21 -7
- package/skills/handle-login-and-logout/SKILL.md +1 -1
- package/skills/login-with-providers/SKILL.md +154 -0
- package/skills/manage-tokens/SKILL.md +7 -1
- package/skills/overview/SKILL.md +1 -1
- package/skills/protect-routes/SKILL.md +71 -13
- package/skills/register-user/SKILL.md +6 -8
- package/skills/run-auth-commands/SKILL.md +1 -1
- package/skills/verify-email-and-reset-password/SKILL.md +137 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { authConfig } from "../services/auth-config.mjs";
|
|
2
|
+
import { InvalidPasskeyError } from "../errors/invalid-passkey.error.mjs";
|
|
3
|
+
import { hashOneTimeToken, oneTimeTokenModel } from "../services/one-time-tokens.mjs";
|
|
4
|
+
|
|
5
|
+
//#region ../auth/src/passkeys/passkey-challenges.ts
|
|
6
|
+
/**
|
|
7
|
+
* Store a ceremony challenge server-side: SHA-256 hashed, purpose-bound,
|
|
8
|
+
* expiring (`auth.passkeys.challengeExpiresIn`, default 5m). A registration
|
|
9
|
+
* challenge is bound to its user; an authentication challenge has none.
|
|
10
|
+
*/
|
|
11
|
+
async function storePasskeyChallenge(ceremony, challenge, user) {
|
|
12
|
+
const expiresAt = new Date(Date.now() + authConfig.passkeys.challengeExpiresInMs());
|
|
13
|
+
await oneTimeTokenModel().issueChallenge(ceremony, hashOneTimeToken(challenge), expiresAt, user);
|
|
14
|
+
}
|
|
15
|
+
/** The challenge the browser signed, read from `clientDataJSON`. */
|
|
16
|
+
function signedChallenge(credential) {
|
|
17
|
+
try {
|
|
18
|
+
const clientData = JSON.parse(Buffer.from(credential.response.clientDataJSON, "base64url").toString("utf8"));
|
|
19
|
+
if (typeof clientData.challenge === "string" && clientData.challenge.length > 0) return clientData.challenge;
|
|
20
|
+
} catch {}
|
|
21
|
+
throw new InvalidPasskeyError("malformed-client-data");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Consume the challenge a credential response signed. It is consumed BEFORE
|
|
25
|
+
* the signature is verified, so a challenge is single-use even when that
|
|
26
|
+
* verification fails. Unknown, other-ceremony, expired and already-used
|
|
27
|
+
* challenges all throw {@link InvalidPasskeyError}.
|
|
28
|
+
*/
|
|
29
|
+
async function takePasskeyChallenge(ceremony, credential) {
|
|
30
|
+
const challenge = signedChallenge(credential);
|
|
31
|
+
const row = await oneTimeTokenModel().findByHash(hashOneTimeToken(challenge), ceremony);
|
|
32
|
+
if (!row || row.isConsumed || row.isExpired) throw new InvalidPasskeyError("unknown-or-expired-challenge");
|
|
33
|
+
if (!await row.consumeIfActive()) throw new InvalidPasskeyError("challenge-already-used");
|
|
34
|
+
return {
|
|
35
|
+
challenge,
|
|
36
|
+
row
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* CSRF rule for a passkey verify: the request's `Origin` header must be one
|
|
41
|
+
* of `auth.passkeys.origin`. The same list is what the signed
|
|
42
|
+
* `clientDataJSON.origin` is checked against.
|
|
43
|
+
*/
|
|
44
|
+
function assertPasskeyOrigin(request) {
|
|
45
|
+
const origins = authConfig.passkeys.origins();
|
|
46
|
+
const origin = request.origin;
|
|
47
|
+
if (typeof origin !== "string" || !origins.includes(origin)) throw new InvalidPasskeyError("origin-mismatch");
|
|
48
|
+
return origins;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
export { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge };
|
|
53
|
+
//# sourceMappingURL=passkey-challenges.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-challenges.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-challenges.ts"],"sourcesContent":["import type { Request } from \"@warlock.js/core\";\nimport { InvalidPasskeyError } from \"../errors/invalid-passkey.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport type { OneTimeToken, OneTimeTokenPurpose } from \"../models/one-time-token\";\nimport { authConfig } from \"../services/auth-config\";\nimport { hashOneTimeToken, oneTimeTokenModel } from \"../services/one-time-tokens\";\nimport type { PasskeyResponseJSON } from \"./simplewebauthn\";\n\n/** Which ceremony a challenge belongs to. */\nexport type PasskeyCeremony = Extract<\n OneTimeTokenPurpose,\n \"passkey-registration\" | \"passkey-authentication\"\n>;\n\n/**\n * Store a ceremony challenge server-side: SHA-256 hashed, purpose-bound,\n * expiring (`auth.passkeys.challengeExpiresIn`, default 5m). A registration\n * challenge is bound to its user; an authentication challenge has none.\n */\nexport async function storePasskeyChallenge(\n ceremony: PasskeyCeremony,\n challenge: string,\n user?: Auth,\n): Promise<void> {\n const expiresAt = new Date(Date.now() + authConfig.passkeys.challengeExpiresInMs());\n\n await oneTimeTokenModel().issueChallenge(ceremony, hashOneTimeToken(challenge), expiresAt, user);\n}\n\n/** The challenge the browser signed, read from `clientDataJSON`. */\nfunction signedChallenge(credential: PasskeyResponseJSON): string {\n try {\n const clientData = JSON.parse(\n Buffer.from(credential.response.clientDataJSON, \"base64url\").toString(\"utf8\"),\n ) as { challenge?: unknown };\n\n if (typeof clientData.challenge === \"string\" && clientData.challenge.length > 0) {\n return clientData.challenge;\n }\n } catch {\n // fall through to the rejection\n }\n\n throw new InvalidPasskeyError(\"malformed-client-data\");\n}\n\n/**\n * Consume the challenge a credential response signed. It is consumed BEFORE\n * the signature is verified, so a challenge is single-use even when that\n * verification fails. Unknown, other-ceremony, expired and already-used\n * challenges all throw {@link InvalidPasskeyError}.\n */\nexport async function takePasskeyChallenge(\n ceremony: PasskeyCeremony,\n credential: PasskeyResponseJSON,\n): Promise<{ challenge: string; row: OneTimeToken }> {\n const challenge = signedChallenge(credential);\n const row = await oneTimeTokenModel().findByHash(hashOneTimeToken(challenge), ceremony);\n\n if (!row || row.isConsumed || row.isExpired) {\n throw new InvalidPasskeyError(\"unknown-or-expired-challenge\");\n }\n\n if (!(await row.consumeIfActive())) {\n throw new InvalidPasskeyError(\"challenge-already-used\");\n }\n\n return { challenge, row };\n}\n\n/**\n * CSRF rule for a passkey verify: the request's `Origin` header must be one\n * of `auth.passkeys.origin`. The same list is what the signed\n * `clientDataJSON.origin` is checked against.\n */\nexport function assertPasskeyOrigin(request: Request): string[] {\n const origins = authConfig.passkeys.origins();\n const origin = request.origin;\n\n if (typeof origin !== \"string\" || !origins.includes(origin)) {\n throw new InvalidPasskeyError(\"origin-mismatch\");\n }\n\n return origins;\n}\n"],"mappings":";;;;;;;;;;AAmBA,eAAsB,sBACpB,UACA,WACA,MACe;CACf,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,WAAW,SAAS,qBAAqB,CAAC;CAElF,MAAM,kBAAkB,CAAC,CAAC,eAAe,UAAU,iBAAiB,SAAS,GAAG,WAAW,IAAI;AACjG;;AAGA,SAAS,gBAAgB,YAAyC;CAChE,IAAI;EACF,MAAM,aAAa,KAAK,MACtB,OAAO,KAAK,WAAW,SAAS,gBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAC9E;EAEA,IAAI,OAAO,WAAW,cAAc,YAAY,WAAW,UAAU,SAAS,GAC5E,OAAO,WAAW;CAEtB,QAAQ,CAER;CAEA,MAAM,IAAI,oBAAoB,uBAAuB;AACvD;;;;;;;AAQA,eAAsB,qBACpB,UACA,YACmD;CACnD,MAAM,YAAY,gBAAgB,UAAU;CAC5C,MAAM,MAAM,MAAM,kBAAkB,CAAC,CAAC,WAAW,iBAAiB,SAAS,GAAG,QAAQ;CAEtF,IAAI,CAAC,OAAO,IAAI,cAAc,IAAI,WAChC,MAAM,IAAI,oBAAoB,8BAA8B;CAG9D,IAAI,CAAE,MAAM,IAAI,gBAAgB,GAC9B,MAAM,IAAI,oBAAoB,wBAAwB;CAGxD,OAAO;EAAE;EAAW;CAAI;AAC1B;;;;;;AAOA,SAAgB,oBAAoB,SAA4B;CAC9D,MAAM,UAAU,WAAW,SAAS,QAAQ;CAC5C,MAAM,SAAS,QAAQ;CAEvB,IAAI,OAAO,WAAW,YAAY,CAAC,QAAQ,SAAS,MAAM,GACxD,MAAM,IAAI,oBAAoB,iBAAiB;CAGjD,OAAO;AACT"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Auth } from "../models/auth.model.mjs";
|
|
2
|
+
import { PasskeyCredential } from "../models/passkey-credential/passkey-credential.model.mjs";
|
|
3
|
+
import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./simplewebauthn.mjs";
|
|
4
|
+
import { Request } from "@warlock.js/core";
|
|
5
|
+
|
|
6
|
+
//#region ../auth/src/passkeys/passkey-registration.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Registration options for a LOGGED-IN user adding a passkey. The challenge is
|
|
9
|
+
* stored single-use and bound to this user; the user's existing credentials
|
|
10
|
+
* are excluded so the same authenticator is not registered twice.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // POST /auth/passkeys/register/options (authMiddleware)
|
|
14
|
+
* response.success(await generatePasskeyRegistrationOptions(request.locals.user));
|
|
15
|
+
*/
|
|
16
|
+
declare function generatePasskeyRegistrationOptions(user: Auth): Promise<PasskeyOptionsJSON>;
|
|
17
|
+
/**
|
|
18
|
+
* Verify the browser's registration response and store the credential.
|
|
19
|
+
* Checks, in order: the request `Origin` (CSRF), the single-use challenge
|
|
20
|
+
* (consumed even if what follows fails) belongs to THIS user, then the
|
|
21
|
+
* attestation via `@simplewebauthn/server` against the configured origins and
|
|
22
|
+
* rpID.
|
|
23
|
+
*
|
|
24
|
+
* @throws InvalidPasskeyError (400, EC011) on any rejection.
|
|
25
|
+
*/
|
|
26
|
+
declare function verifyPasskeyRegistration(request: Request, user: Auth, credential: PasskeyResponseJSON): Promise<PasskeyCredential>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { generatePasskeyRegistrationOptions, verifyPasskeyRegistration };
|
|
29
|
+
//# sourceMappingURL=passkey-registration.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-registration.d.mts","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-registration.ts"],"mappings":";;;;;;;;;AAoCA;;;;;;iBAAsB,kCAAA,CAAmC,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,kBAAA;;;;;;;AAAkB;AAgChG;;iBAAsB,yBAAA,CACpB,OAAA,EAAS,OAAA,EACT,IAAA,EAAM,IAAA,EACN,UAAA,EAAY,mBAAA,GACX,OAAA,CAAQ,iBAAA"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { authConfig } from "../services/auth-config.mjs";
|
|
2
|
+
import { InvalidPasskeyError } from "../errors/invalid-passkey.error.mjs";
|
|
3
|
+
import { PasskeyCredential } from "../models/passkey-credential/passkey-credential.model.mjs";
|
|
4
|
+
import "../models/passkey-credential/index.mjs";
|
|
5
|
+
import { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge } from "./passkey-challenges.mjs";
|
|
6
|
+
import { loadSimpleWebAuthn } from "./simplewebauthn.mjs";
|
|
7
|
+
|
|
8
|
+
//#region ../auth/src/passkeys/passkey-registration.ts
|
|
9
|
+
/** Account name the authenticator shows: `auth.passkeys.userName`, else email, else id. */
|
|
10
|
+
function passkeyUserName(user) {
|
|
11
|
+
const configured = authConfig.passkeys.settings().userName;
|
|
12
|
+
if (configured) return configured(user);
|
|
13
|
+
const email = user.get("email");
|
|
14
|
+
return typeof email === "string" && email ? email : String(user.id);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Registration options for a LOGGED-IN user adding a passkey. The challenge is
|
|
18
|
+
* stored single-use and bound to this user; the user's existing credentials
|
|
19
|
+
* are excluded so the same authenticator is not registered twice.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // POST /auth/passkeys/register/options (authMiddleware)
|
|
23
|
+
* response.success(await generatePasskeyRegistrationOptions(request.locals.user));
|
|
24
|
+
*/
|
|
25
|
+
async function generatePasskeyRegistrationOptions(user) {
|
|
26
|
+
const webauthn = await loadSimpleWebAuthn();
|
|
27
|
+
const settings = authConfig.passkeys.settings();
|
|
28
|
+
const existing = await PasskeyCredential.listFor(user);
|
|
29
|
+
const options = await webauthn.generateRegistrationOptions({
|
|
30
|
+
rpName: settings.rpName,
|
|
31
|
+
rpID: settings.rpID,
|
|
32
|
+
userName: passkeyUserName(user),
|
|
33
|
+
userID: new TextEncoder().encode(`${user.userType}:${user.id}`),
|
|
34
|
+
attestationType: "none",
|
|
35
|
+
excludeCredentials: existing.map((credential) => ({
|
|
36
|
+
id: credential.credentialId,
|
|
37
|
+
transports: credential.get("transports") ?? void 0
|
|
38
|
+
})),
|
|
39
|
+
authenticatorSelection: {
|
|
40
|
+
residentKey: "preferred",
|
|
41
|
+
userVerification: "preferred"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
await storePasskeyChallenge("passkey-registration", options.challenge, user);
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Verify the browser's registration response and store the credential.
|
|
49
|
+
* Checks, in order: the request `Origin` (CSRF), the single-use challenge
|
|
50
|
+
* (consumed even if what follows fails) belongs to THIS user, then the
|
|
51
|
+
* attestation via `@simplewebauthn/server` against the configured origins and
|
|
52
|
+
* rpID.
|
|
53
|
+
*
|
|
54
|
+
* @throws InvalidPasskeyError (400, EC011) on any rejection.
|
|
55
|
+
*/
|
|
56
|
+
async function verifyPasskeyRegistration(request, user, credential) {
|
|
57
|
+
const webauthn = await loadSimpleWebAuthn();
|
|
58
|
+
const origins = assertPasskeyOrigin(request);
|
|
59
|
+
const { challenge, row } = await takePasskeyChallenge("passkey-registration", credential);
|
|
60
|
+
if (String(row.userId) !== String(user.id) || row.userType !== user.userType) throw new InvalidPasskeyError("challenge-issued-to-another-user");
|
|
61
|
+
let verification;
|
|
62
|
+
try {
|
|
63
|
+
verification = await webauthn.verifyRegistrationResponse({
|
|
64
|
+
response: credential,
|
|
65
|
+
expectedChallenge: challenge,
|
|
66
|
+
expectedOrigin: origins,
|
|
67
|
+
expectedRPID: authConfig.passkeys.settings().rpID,
|
|
68
|
+
requireUserVerification: false
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new InvalidPasskeyError("registration-verification-failed", error);
|
|
72
|
+
}
|
|
73
|
+
if (!verification.verified || !verification.registrationInfo) throw new InvalidPasskeyError("registration-not-verified");
|
|
74
|
+
const { id, publicKey, counter, transports } = verification.registrationInfo.credential;
|
|
75
|
+
if (await PasskeyCredential.findByCredentialId(id)) throw new InvalidPasskeyError("credential-already-registered");
|
|
76
|
+
return PasskeyCredential.create({
|
|
77
|
+
credential_id: id,
|
|
78
|
+
public_key: Buffer.from(publicKey).toString("base64url"),
|
|
79
|
+
counter,
|
|
80
|
+
transports: transports ?? credential.response.transports ?? [],
|
|
81
|
+
user_id: user.id,
|
|
82
|
+
user_type: user.userType
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
export { generatePasskeyRegistrationOptions, verifyPasskeyRegistration };
|
|
88
|
+
//# sourceMappingURL=passkey-registration.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-registration.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-registration.ts"],"sourcesContent":["import type { Request } from \"@warlock.js/core\";\nimport { InvalidPasskeyError } from \"../errors/invalid-passkey.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { PasskeyCredential } from \"../models/passkey-credential\";\nimport { authConfig } from \"../services/auth-config\";\nimport {\n assertPasskeyOrigin,\n storePasskeyChallenge,\n takePasskeyChallenge,\n} from \"./passkey-challenges\";\nimport {\n loadSimpleWebAuthn,\n type PasskeyOptionsJSON,\n type PasskeyResponseJSON,\n} from \"./simplewebauthn\";\n\n/** Account name the authenticator shows: `auth.passkeys.userName`, else email, else id. */\nfunction passkeyUserName(user: Auth): string {\n const configured = authConfig.passkeys.settings().userName;\n\n if (configured) return configured(user);\n\n const email = user.get(\"email\");\n\n return typeof email === \"string\" && email ? email : String(user.id);\n}\n\n/**\n * Registration options for a LOGGED-IN user adding a passkey. The challenge is\n * stored single-use and bound to this user; the user's existing credentials\n * are excluded so the same authenticator is not registered twice.\n *\n * @example\n * // POST /auth/passkeys/register/options (authMiddleware)\n * response.success(await generatePasskeyRegistrationOptions(request.locals.user));\n */\nexport async function generatePasskeyRegistrationOptions(user: Auth): Promise<PasskeyOptionsJSON> {\n const webauthn = await loadSimpleWebAuthn();\n const settings = authConfig.passkeys.settings();\n const existing = await PasskeyCredential.listFor(user);\n\n const options = await webauthn.generateRegistrationOptions({\n rpName: settings.rpName,\n rpID: settings.rpID,\n userName: passkeyUserName(user),\n userID: new TextEncoder().encode(`${user.userType}:${user.id}`),\n attestationType: \"none\",\n excludeCredentials: existing.map((credential) => ({\n id: credential.credentialId,\n transports: credential.get(\"transports\") ?? undefined,\n })),\n authenticatorSelection: { residentKey: \"preferred\", userVerification: \"preferred\" },\n });\n\n await storePasskeyChallenge(\"passkey-registration\", options.challenge, user);\n\n return options;\n}\n\n/**\n * Verify the browser's registration response and store the credential.\n * Checks, in order: the request `Origin` (CSRF), the single-use challenge\n * (consumed even if what follows fails) belongs to THIS user, then the\n * attestation via `@simplewebauthn/server` against the configured origins and\n * rpID.\n *\n * @throws InvalidPasskeyError (400, EC011) on any rejection.\n */\nexport async function verifyPasskeyRegistration(\n request: Request,\n user: Auth,\n credential: PasskeyResponseJSON,\n): Promise<PasskeyCredential> {\n const webauthn = await loadSimpleWebAuthn();\n const origins = assertPasskeyOrigin(request);\n const { challenge, row } = await takePasskeyChallenge(\"passkey-registration\", credential);\n\n if (String(row.userId) !== String(user.id) || row.userType !== user.userType) {\n throw new InvalidPasskeyError(\"challenge-issued-to-another-user\");\n }\n\n let verification: Awaited<ReturnType<typeof webauthn.verifyRegistrationResponse>>;\n\n try {\n verification = await webauthn.verifyRegistrationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin: origins,\n expectedRPID: authConfig.passkeys.settings().rpID,\n requireUserVerification: false,\n });\n } catch (error) {\n throw new InvalidPasskeyError(\"registration-verification-failed\", error);\n }\n\n if (!verification.verified || !verification.registrationInfo) {\n throw new InvalidPasskeyError(\"registration-not-verified\");\n }\n\n const { id, publicKey, counter, transports } = verification.registrationInfo.credential;\n\n if (await PasskeyCredential.findByCredentialId(id)) {\n throw new InvalidPasskeyError(\"credential-already-registered\");\n }\n\n return PasskeyCredential.create({\n credential_id: id,\n public_key: Buffer.from(publicKey).toString(\"base64url\"),\n counter,\n transports: transports ?? credential.response.transports ?? [],\n user_id: user.id,\n user_type: user.userType,\n });\n}\n"],"mappings":";;;;;;;;;AAiBA,SAAS,gBAAgB,MAAoB;CAC3C,MAAM,aAAa,WAAW,SAAS,SAAS,CAAC,CAAC;CAElD,IAAI,YAAY,OAAO,WAAW,IAAI;CAEtC,MAAM,QAAQ,KAAK,IAAI,OAAO;CAE9B,OAAO,OAAO,UAAU,YAAY,QAAQ,QAAQ,OAAO,KAAK,EAAE;AACpE;;;;;;;;;;AAWA,eAAsB,mCAAmC,MAAyC;CAChG,MAAM,WAAW,MAAM,mBAAmB;CAC1C,MAAM,WAAW,WAAW,SAAS,SAAS;CAC9C,MAAM,WAAW,MAAM,kBAAkB,QAAQ,IAAI;CAErD,MAAM,UAAU,MAAM,SAAS,4BAA4B;EACzD,QAAQ,SAAS;EACjB,MAAM,SAAS;EACf,UAAU,gBAAgB,IAAI;EAC9B,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,GAAG,KAAK,SAAS,GAAG,KAAK,IAAI;EAC9D,iBAAiB;EACjB,oBAAoB,SAAS,KAAK,gBAAgB;GAChD,IAAI,WAAW;GACf,YAAY,WAAW,IAAI,YAAY,KAAK;EAC9C,EAAE;EACF,wBAAwB;GAAE,aAAa;GAAa,kBAAkB;EAAY;CACpF,CAAC;CAED,MAAM,sBAAsB,wBAAwB,QAAQ,WAAW,IAAI;CAE3E,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,0BACpB,SACA,MACA,YAC4B;CAC5B,MAAM,WAAW,MAAM,mBAAmB;CAC1C,MAAM,UAAU,oBAAoB,OAAO;CAC3C,MAAM,EAAE,WAAW,QAAQ,MAAM,qBAAqB,wBAAwB,UAAU;CAExF,IAAI,OAAO,IAAI,MAAM,MAAM,OAAO,KAAK,EAAE,KAAK,IAAI,aAAa,KAAK,UAClE,MAAM,IAAI,oBAAoB,kCAAkC;CAGlE,IAAI;CAEJ,IAAI;EACF,eAAe,MAAM,SAAS,2BAA2B;GACvD,UAAU;GACV,mBAAmB;GACnB,gBAAgB;GAChB,cAAc,WAAW,SAAS,SAAS,CAAC,CAAC;GAC7C,yBAAyB;EAC3B,CAAC;CACH,SAAS,OAAO;EACd,MAAM,IAAI,oBAAoB,oCAAoC,KAAK;CACzE;CAEA,IAAI,CAAC,aAAa,YAAY,CAAC,aAAa,kBAC1C,MAAM,IAAI,oBAAoB,2BAA2B;CAG3D,MAAM,EAAE,IAAI,WAAW,SAAS,eAAe,aAAa,iBAAiB;CAE7E,IAAI,MAAM,kBAAkB,mBAAmB,EAAE,GAC/C,MAAM,IAAI,oBAAoB,+BAA+B;CAG/D,OAAO,kBAAkB,OAAO;EAC9B,eAAe;EACf,YAAY,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,WAAW;EACvD;EACA,YAAY,cAAc,WAAW,SAAS,cAAc,CAAC;EAC7D,SAAS,KAAK;EACd,WAAW,KAAK;CAClB,CAAC;AACH"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region ../auth/src/passkeys/simplewebauthn.d.ts
|
|
2
|
+
/** The browser's JSON-serialized credential (`startRegistration` / `startAuthentication` result). */
|
|
3
|
+
type PasskeyResponseJSON = {
|
|
4
|
+
id: string;
|
|
5
|
+
rawId: string;
|
|
6
|
+
type: string;
|
|
7
|
+
response: {
|
|
8
|
+
clientDataJSON: string;
|
|
9
|
+
transports?: string[];
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
/** Options JSON handed to the browser; `challenge` is what auth stores. */
|
|
15
|
+
type PasskeyOptionsJSON = {
|
|
16
|
+
challenge: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { PasskeyOptionsJSON, PasskeyResponseJSON };
|
|
21
|
+
//# sourceMappingURL=simplewebauthn.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simplewebauthn.d.mts","names":[],"sources":["../../../../../../../auth/src/passkeys/simplewebauthn.ts"],"mappings":";;KAMY,mBAAA;EACV,EAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;IACE,cAAA;IACA,UAAA;IAAA,CACC,GAAA;EAAA;EAAA,CAEF,GAAA;AAAA;AAAW;AAAA,KAIF,kBAAA;EACV,SAAA;EAAA,CACC,GAAW;AAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { loadOptionalPeer } from "../services/optional-peer.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../auth/src/passkeys/simplewebauthn.ts
|
|
4
|
+
/** The `warlock add` feature that installs `@simplewebauthn/server`. */
|
|
5
|
+
const PASSKEYS_FEATURE = "auth-passkeys";
|
|
6
|
+
/** Load `@simplewebauthn/server`, or throw `AuthProviderSdkMissingError` naming `warlock add auth-passkeys`. */
|
|
7
|
+
function loadSimpleWebAuthn() {
|
|
8
|
+
return loadOptionalPeer("@simplewebauthn/server", PASSKEYS_FEATURE);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { loadSimpleWebAuthn };
|
|
13
|
+
//# sourceMappingURL=simplewebauthn.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simplewebauthn.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/simplewebauthn.ts"],"sourcesContent":["import { loadOptionalPeer } from \"../services/optional-peer\";\n\n/** The `warlock add` feature that installs `@simplewebauthn/server`. */\nexport const PASSKEYS_FEATURE = \"auth-passkeys\";\n\n/** The browser's JSON-serialized credential (`startRegistration` / `startAuthentication` result). */\nexport type PasskeyResponseJSON = {\n id: string;\n rawId: string;\n type: string;\n response: {\n clientDataJSON: string;\n transports?: string[];\n [key: string]: unknown;\n };\n [key: string]: unknown;\n};\n\n/** Options JSON handed to the browser; `challenge` is what auth stores. */\nexport type PasskeyOptionsJSON = {\n challenge: string;\n [key: string]: unknown;\n};\n\n/** A stored credential in the shape `@simplewebauthn/server` verifies against. */\nexport type WebAuthnCredential = {\n id: string;\n publicKey: Uint8Array;\n counter: number;\n transports?: string[];\n};\n\n/**\n * The slice of `@simplewebauthn/server` (v13) auth uses — declared locally\n * because the package is an optional peer.\n */\nexport type SimpleWebAuthnServer = {\n generateRegistrationOptions: (options: Record<string, unknown>) => Promise<PasskeyOptionsJSON>;\n verifyRegistrationResponse: (options: Record<string, unknown>) => Promise<{\n verified: boolean;\n registrationInfo?: { credential: WebAuthnCredential };\n }>;\n generateAuthenticationOptions: (options: Record<string, unknown>) => Promise<PasskeyOptionsJSON>;\n verifyAuthenticationResponse: (options: Record<string, unknown>) => Promise<{\n verified: boolean;\n authenticationInfo: { newCounter: number };\n }>;\n};\n\n/** Load `@simplewebauthn/server`, or throw `AuthProviderSdkMissingError` naming `warlock add auth-passkeys`. */\nexport function loadSimpleWebAuthn(): Promise<SimpleWebAuthnServer> {\n return loadOptionalPeer<SimpleWebAuthnServer>(\"@simplewebauthn/server\", PASSKEYS_FEATURE);\n}\n"],"mappings":";;;;AAGA,MAAa,mBAAmB;;AA+ChC,SAAgB,qBAAoD;CAClE,OAAO,iBAAuC,0BAA0B,gBAAgB;AAC1F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AuthProvider, GoogleProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../auth/src/providers/google-provider.d.ts
|
|
4
|
+
/** The `warlock add` feature that installs `jose`. */
|
|
5
|
+
declare const GOOGLE_AUTH_FEATURE = "auth-google";
|
|
6
|
+
/** The slice of `jose` the Google provider uses — declared locally because `jose` is an optional peer. */
|
|
7
|
+
type JoseModule = {
|
|
8
|
+
createRemoteJWKSet: (url: URL) => unknown;
|
|
9
|
+
jwtVerify: (token: string, key: unknown, options: {
|
|
10
|
+
issuer: string[];
|
|
11
|
+
audience: string;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
payload: Record<string, unknown>;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
/** PKCE S256 code challenge for a verifier. */
|
|
17
|
+
declare function pkceChallenge(codeVerifier: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Google sign-in: OpenID Connect authorization code flow with PKCE (S256),
|
|
20
|
+
* `state` and `nonce`. The code is exchanged with plain `fetch`; the id_token
|
|
21
|
+
* is verified with `jose` against Google's JWKS (signature, `iss`, `aud` = the
|
|
22
|
+
* client id, `exp`), then its `nonce` is compared. No Google SDK.
|
|
23
|
+
*/
|
|
24
|
+
declare class GoogleProvider implements AuthProvider {
|
|
25
|
+
private readonly settings;
|
|
26
|
+
readonly name = "google";
|
|
27
|
+
constructor(settings: GoogleProviderConfig);
|
|
28
|
+
authorizationUrl(state: ProviderAuthorizationState): string;
|
|
29
|
+
handleCallback({
|
|
30
|
+
query,
|
|
31
|
+
expected
|
|
32
|
+
}: ProviderCallbackParams): Promise<ProviderProfile>;
|
|
33
|
+
/** Redeem the authorization code (with the PKCE verifier) for an id_token. */
|
|
34
|
+
private exchangeCode;
|
|
35
|
+
/** Verify signature, issuer, audience and expiry; resolve the claims. */
|
|
36
|
+
private verifyIdToken;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { GOOGLE_AUTH_FEATURE, GoogleProvider, JoseModule, pkceChallenge };
|
|
40
|
+
//# sourceMappingURL=google-provider.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/google-provider.ts"],"mappings":";;;;cAkBa,mBAAA;AAAb;AAAA,KAGY,UAAA;EACV,kBAAA,GAAqB,GAAA,EAAK,GAAA;EAC1B,SAAA,GACE,KAAA,UACA,GAAA,WACA,OAAA;IAAW,MAAA;IAAkB,QAAA;EAAA,MAC1B,OAAA;IAAU,OAAA,EAAS,MAAA;EAAA;AAAA;;iBAOV,aAAA,CAAc,YAAoB;;;;;;;cAUrC,cAAA,YAA0B,YAAA;EAAA,iBAGD,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,oBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAelB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAxCZ;EAAA,QA0EV,YAAA;EA1EgB;EAAA,QA4GhB,aAAA;AAAA"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
|
|
2
|
+
import { loadOptionalPeer } from "../services/optional-peer.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
//#region ../auth/src/providers/google-provider.ts
|
|
6
|
+
const AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
7
|
+
const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
8
|
+
const JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
9
|
+
const ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
10
|
+
const DEFAULT_SCOPES = [
|
|
11
|
+
"openid",
|
|
12
|
+
"email",
|
|
13
|
+
"profile"
|
|
14
|
+
];
|
|
15
|
+
/** The `warlock add` feature that installs `jose`. */
|
|
16
|
+
const GOOGLE_AUTH_FEATURE = "auth-google";
|
|
17
|
+
/** Google's key set, created once per process; `jose` caches the fetched JWKS inside it. */
|
|
18
|
+
let googleKeySet;
|
|
19
|
+
/** PKCE S256 code challenge for a verifier. */
|
|
20
|
+
function pkceChallenge(codeVerifier) {
|
|
21
|
+
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Google sign-in: OpenID Connect authorization code flow with PKCE (S256),
|
|
25
|
+
* `state` and `nonce`. The code is exchanged with plain `fetch`; the id_token
|
|
26
|
+
* is verified with `jose` against Google's JWKS (signature, `iss`, `aud` = the
|
|
27
|
+
* client id, `exp`), then its `nonce` is compared. No Google SDK.
|
|
28
|
+
*/
|
|
29
|
+
var GoogleProvider = class {
|
|
30
|
+
constructor(settings) {
|
|
31
|
+
this.settings = settings;
|
|
32
|
+
this.name = "google";
|
|
33
|
+
}
|
|
34
|
+
authorizationUrl(state) {
|
|
35
|
+
const url = new URL(AUTHORIZATION_ENDPOINT);
|
|
36
|
+
url.searchParams.set("client_id", this.settings.clientId);
|
|
37
|
+
url.searchParams.set("redirect_uri", this.settings.redirectUri);
|
|
38
|
+
url.searchParams.set("response_type", "code");
|
|
39
|
+
url.searchParams.set("scope", (this.settings.scopes ?? DEFAULT_SCOPES).join(" "));
|
|
40
|
+
url.searchParams.set("state", state.state);
|
|
41
|
+
url.searchParams.set("nonce", state.nonce);
|
|
42
|
+
url.searchParams.set("code_challenge", pkceChallenge(state.codeVerifier));
|
|
43
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
44
|
+
return url.toString();
|
|
45
|
+
}
|
|
46
|
+
async handleCallback({ query, expected }) {
|
|
47
|
+
const jose = await loadOptionalPeer("jose", GOOGLE_AUTH_FEATURE);
|
|
48
|
+
const code = query.code;
|
|
49
|
+
if (typeof code !== "string" || code.length === 0) throw new InvalidProviderCallbackError("missing-code");
|
|
50
|
+
const idToken = await this.exchangeCode(code, expected.codeVerifier);
|
|
51
|
+
const claims = await this.verifyIdToken(jose, idToken);
|
|
52
|
+
if (typeof claims.nonce !== "string" || claims.nonce !== expected.nonce) throw new InvalidProviderCallbackError("nonce-mismatch");
|
|
53
|
+
if (typeof claims.sub !== "string" || claims.sub.length === 0) throw new InvalidProviderCallbackError("missing-subject");
|
|
54
|
+
return {
|
|
55
|
+
provider: this.name,
|
|
56
|
+
providerUserId: claims.sub,
|
|
57
|
+
email: typeof claims.email === "string" ? claims.email : void 0,
|
|
58
|
+
emailVerified: claims.email_verified === true,
|
|
59
|
+
name: typeof claims.name === "string" ? claims.name : void 0,
|
|
60
|
+
avatar: typeof claims.picture === "string" ? claims.picture : void 0,
|
|
61
|
+
raw: claims
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Redeem the authorization code (with the PKCE verifier) for an id_token. */
|
|
65
|
+
async exchangeCode(code, codeVerifier) {
|
|
66
|
+
let body;
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(TOKEN_ENDPOINT, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
71
|
+
body: new URLSearchParams({
|
|
72
|
+
code,
|
|
73
|
+
client_id: this.settings.clientId,
|
|
74
|
+
client_secret: this.settings.clientSecret,
|
|
75
|
+
redirect_uri: this.settings.redirectUri,
|
|
76
|
+
grant_type: "authorization_code",
|
|
77
|
+
code_verifier: codeVerifier
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) throw new Error(`Google token endpoint answered ${response.status}`);
|
|
81
|
+
body = await response.json();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new InvalidProviderCallbackError("code-exchange-failed", error);
|
|
84
|
+
}
|
|
85
|
+
if (typeof body.id_token !== "string") throw new InvalidProviderCallbackError("missing-id-token");
|
|
86
|
+
return body.id_token;
|
|
87
|
+
}
|
|
88
|
+
/** Verify signature, issuer, audience and expiry; resolve the claims. */
|
|
89
|
+
async verifyIdToken(jose, idToken) {
|
|
90
|
+
googleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));
|
|
91
|
+
try {
|
|
92
|
+
const { payload } = await jose.jwtVerify(idToken, googleKeySet, {
|
|
93
|
+
issuer: ISSUERS,
|
|
94
|
+
audience: this.settings.clientId
|
|
95
|
+
});
|
|
96
|
+
return payload;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new InvalidProviderCallbackError("invalid-id-token", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { GOOGLE_AUTH_FEATURE, GoogleProvider, pkceChallenge };
|
|
105
|
+
//# sourceMappingURL=google-provider.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/google-provider.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type {\n AuthProvider,\n GoogleProviderConfig,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { loadOptionalPeer } from \"../services/optional-peer\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst TOKEN_ENDPOINT = \"https://oauth2.googleapis.com/token\";\nconst JWKS_URI = \"https://www.googleapis.com/oauth2/v3/certs\";\nconst ISSUERS = [\"https://accounts.google.com\", \"accounts.google.com\"];\nconst DEFAULT_SCOPES = [\"openid\", \"email\", \"profile\"];\n\n/** The `warlock add` feature that installs `jose`. */\nexport const GOOGLE_AUTH_FEATURE = \"auth-google\";\n\n/** The slice of `jose` the Google provider uses — declared locally because `jose` is an optional peer. */\nexport type JoseModule = {\n createRemoteJWKSet: (url: URL) => unknown;\n jwtVerify: (\n token: string,\n key: unknown,\n options: { issuer: string[]; audience: string },\n ) => Promise<{ payload: Record<string, unknown> }>;\n};\n\n/** Google's key set, created once per process; `jose` caches the fetched JWKS inside it. */\nlet googleKeySet: unknown;\n\n/** PKCE S256 code challenge for a verifier. */\nexport function pkceChallenge(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n}\n\n/**\n * Google sign-in: OpenID Connect authorization code flow with PKCE (S256),\n * `state` and `nonce`. The code is exchanged with plain `fetch`; the id_token\n * is verified with `jose` against Google's JWKS (signature, `iss`, `aud` = the\n * client id, `exp`), then its `nonce` is compared. No Google SDK.\n */\nexport class GoogleProvider implements AuthProvider {\n public readonly name = \"google\";\n\n public constructor(private readonly settings: GoogleProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (this.settings.scopes ?? DEFAULT_SCOPES).join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"nonce\", state.nonce);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n // Load the peer first: a missing SDK fails loudly before any network call.\n const jose = await loadOptionalPeer<JoseModule>(\"jose\", GOOGLE_AUTH_FEATURE);\n\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const idToken = await this.exchangeCode(code, expected.codeVerifier);\n const claims = await this.verifyIdToken(jose, idToken);\n\n if (typeof claims.nonce !== \"string\" || claims.nonce !== expected.nonce) {\n throw new InvalidProviderCallbackError(\"nonce-mismatch\");\n }\n\n if (typeof claims.sub !== \"string\" || claims.sub.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: claims.sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n // Google sends a boolean; anything else counts as NOT verified.\n emailVerified: claims.email_verified === true,\n name: typeof claims.name === \"string\" ? claims.name : undefined,\n avatar: typeof claims.picture === \"string\" ? claims.picture : undefined,\n raw: claims,\n };\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an id_token. */\n private async exchangeCode(code: string, codeVerifier: string): Promise<string> {\n let body: { id_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: this.settings.clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Google token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { id_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.id_token !== \"string\") {\n throw new InvalidProviderCallbackError(\"missing-id-token\");\n }\n\n return body.id_token;\n }\n\n /** Verify signature, issuer, audience and expiry; resolve the claims. */\n private async verifyIdToken(jose: JoseModule, idToken: string): Promise<Record<string, unknown>> {\n googleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));\n\n try {\n const { payload } = await jose.jwtVerify(idToken, googleKeySet, {\n issuer: ISSUERS,\n audience: this.settings.clientId,\n });\n\n return payload;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"invalid-id-token\", error);\n }\n }\n}\n"],"mappings":";;;;;AAWA,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,WAAW;AACjB,MAAM,UAAU,CAAC,+BAA+B,qBAAqB;AACrE,MAAM,iBAAiB;CAAC;CAAU;CAAS;AAAS;;AAGpD,MAAa,sBAAsB;;AAanC,IAAI;;AAGJ,SAAgB,cAAc,cAA8B;CAC1D,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,YAAY,CAAC,CAAC,OAAO,WAAW;AACrE;;;;;;;AAQA,IAAa,iBAAb,MAAoD;CAGlD,AAAO,YAAY,AAAiB,UAAgC;EAAhC;cAFb;CAE8C;CAErE,AAAO,iBAAiB,OAA2C;EACjE,MAAM,MAAM,IAAI,IAAI,sBAAsB;EAE1C,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;EACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;EAC9D,IAAI,aAAa,IAAI,iBAAiB,MAAM;EAC5C,IAAI,aAAa,IAAI,UAAU,KAAK,SAAS,UAAU,eAAc,CAAE,KAAK,GAAG,CAAC;EAChF,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,kBAAkB,cAAc,MAAM,YAAY,CAAC;EACxE,IAAI,aAAa,IAAI,yBAAyB,MAAM;EAEpD,OAAO,IAAI,SAAS;CACtB;CAEA,MAAa,eAAe,EAC1B,OACA,YACmD;EAEnD,MAAM,OAAO,MAAM,iBAA6B,QAAQ,mBAAmB;EAE3E,MAAM,OAAO,MAAM;EAEnB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,6BAA6B,cAAc;EAGvD,MAAM,UAAU,MAAM,KAAK,aAAa,MAAM,SAAS,YAAY;EACnE,MAAM,SAAS,MAAM,KAAK,cAAc,MAAM,OAAO;EAErD,IAAI,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,SAAS,OAChE,MAAM,IAAI,6BAA6B,gBAAgB;EAGzD,IAAI,OAAO,OAAO,QAAQ,YAAY,OAAO,IAAI,WAAW,GAC1D,MAAM,IAAI,6BAA6B,iBAAiB;EAG1D,OAAO;GACL,UAAU,KAAK;GACf,gBAAgB,OAAO;GACvB,OAAO,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;GAEzD,eAAe,OAAO,mBAAmB;GACzC,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;GACtD,QAAQ,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;GAC9D,KAAK;EACP;CACF;;CAGA,MAAc,aAAa,MAAc,cAAuC;EAC9E,IAAI;EAEJ,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,gBAAgB;IAC3C,QAAQ;IACR,SAAS,EAAE,gBAAgB,oCAAoC;IAC/D,MAAM,IAAI,gBAAgB;KACxB;KACA,WAAW,KAAK,SAAS;KACzB,eAAe,KAAK,SAAS;KAC7B,cAAc,KAAK,SAAS;KAC5B,YAAY;KACZ,eAAe;IACjB,CAAC;GACH,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kCAAkC,SAAS,QAAQ;GAGrE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;EAEA,IAAI,OAAO,KAAK,aAAa,UAC3B,MAAM,IAAI,6BAA6B,kBAAkB;EAG3D,OAAO,KAAK;CACd;;CAGA,MAAc,cAAc,MAAkB,SAAmD;EAC/F,iBAAiB,KAAK,mBAAmB,IAAI,IAAI,QAAQ,CAAC;EAE1D,IAAI;GACF,MAAM,EAAE,YAAY,MAAM,KAAK,UAAU,SAAS,cAAc;IAC9D,QAAQ;IACR,UAAU,KAAK,SAAS;GAC1B,CAAC;GAED,OAAO;EACT,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,oBAAoB,KAAK;EAClE;CACF;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Auth } from "../models/auth.model.mjs";
|
|
2
|
+
import { AuthProvider } from "../contracts/providers.mjs";
|
|
3
|
+
import { DeviceInfo, LoginResult } from "../contracts/types.mjs";
|
|
4
|
+
import { Request, Response } from "@warlock.js/core";
|
|
5
|
+
import { ChildModel } from "@warlock.js/cascade";
|
|
6
|
+
|
|
7
|
+
//#region ../auth/src/providers/provider-login.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a provider by name: `google` from `auth.providers.google`, anything
|
|
10
|
+
* else from `auth.providers.custom`. Throws naming the config key when absent.
|
|
11
|
+
*/
|
|
12
|
+
declare function resolveAuthProvider(name: string): AuthProvider;
|
|
13
|
+
/**
|
|
14
|
+
* Start a redirect login: generate `state`, `nonce` and a PKCE verifier, store
|
|
15
|
+
* them in a signed 10-minute cookie, and resolve the provider URL to redirect
|
|
16
|
+
* the browser to.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // GET /auth/google
|
|
20
|
+
* export const googleRedirect: RequestHandler = async (_request, response) =>
|
|
21
|
+
* response.redirect(await startProviderLogin(response, "google"));
|
|
22
|
+
*/
|
|
23
|
+
declare function startProviderLogin(response: Response, providerName: string): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Finish a redirect login on the provider's callback: consume the state
|
|
26
|
+
* cookie, reject a missing/forged/expired cookie, a cookie from another
|
|
27
|
+
* provider, a provider-reported error or a `state` mismatch; let the provider
|
|
28
|
+
* verify the callback; find-or-link the user; then log them in through
|
|
29
|
+
* `authService.completeLogin` — the same outcome as password login.
|
|
30
|
+
*
|
|
31
|
+
* @throws InvalidProviderCallbackError (400, EC009) for any callback rejection.
|
|
32
|
+
* @throws ProviderEmailNotVerifiedError (403, EC010) for an unlinked, unverified email.
|
|
33
|
+
* @throws AuthProviderSdkMissingError when the provider's SDK is not installed.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // GET /auth/google/callback
|
|
37
|
+
* const { user, tokens } = await completeProviderLogin(User, "google", request, response);
|
|
38
|
+
* authService.setAuthCookie(response, tokens.accessToken);
|
|
39
|
+
*/
|
|
40
|
+
declare function completeProviderLogin<T extends Auth>(Model: ChildModel<T>, providerName: string, request: Request, response: Response, deviceInfo?: DeviceInfo): Promise<LoginResult<T>>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { completeProviderLogin, resolveAuthProvider, startProviderLogin };
|
|
43
|
+
//# sourceMappingURL=provider-login.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-login.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/provider-login.ts"],"mappings":";;;;;;;;;AAiBA;;iBAAgB,mBAAA,CAAoB,IAAA,WAAe,YAAY;;AAAA;AA4B/D;;;;;;;;iBAAsB,kBAAA,CACpB,QAAA,EAAU,QAAA,EACV,YAAA,WACC,OAAO;AAAA;AAgCV;;;;;;;;;;;;;;;AAhCU,iBAgCY,qBAAA,WAAgC,IAAA,EACpD,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,YAAA,UACA,OAAA,EAAS,OAAA,EACT,QAAA,EAAU,QAAA,EACV,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { authConfig } from "../services/auth-config.mjs";
|
|
2
|
+
import { authService } from "../services/auth.service.mjs";
|
|
3
|
+
import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
|
|
4
|
+
import { safeEqual } from "../utils/safe-equal.mjs";
|
|
5
|
+
import { GoogleProvider } from "./google-provider.mjs";
|
|
6
|
+
import { randomUrlSafe, takeProviderState, writeProviderState } from "./provider-state-cookie.mjs";
|
|
7
|
+
import { resolveProviderUser } from "./resolve-provider-user.mjs";
|
|
8
|
+
|
|
9
|
+
//#region ../auth/src/providers/provider-login.ts
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a provider by name: `google` from `auth.providers.google`, anything
|
|
12
|
+
* else from `auth.providers.custom`. Throws naming the config key when absent.
|
|
13
|
+
*/
|
|
14
|
+
function resolveAuthProvider(name) {
|
|
15
|
+
if (name === "google") {
|
|
16
|
+
const google = authConfig.providers.google();
|
|
17
|
+
if (google) return new GoogleProvider(google);
|
|
18
|
+
} else {
|
|
19
|
+
const custom = authConfig.providers.custom(name);
|
|
20
|
+
if (custom) return custom;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`@warlock.js/auth: no login provider "${name}" — configure \`auth.providers.${name === "google" ? "google" : `custom.${name}`}\`.`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Start a redirect login: generate `state`, `nonce` and a PKCE verifier, store
|
|
26
|
+
* them in a signed 10-minute cookie, and resolve the provider URL to redirect
|
|
27
|
+
* the browser to.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // GET /auth/google
|
|
31
|
+
* export const googleRedirect: RequestHandler = async (_request, response) =>
|
|
32
|
+
* response.redirect(await startProviderLogin(response, "google"));
|
|
33
|
+
*/
|
|
34
|
+
async function startProviderLogin(response, providerName) {
|
|
35
|
+
const provider = resolveAuthProvider(providerName);
|
|
36
|
+
const state = {
|
|
37
|
+
state: randomUrlSafe(),
|
|
38
|
+
nonce: randomUrlSafe(),
|
|
39
|
+
codeVerifier: randomUrlSafe()
|
|
40
|
+
};
|
|
41
|
+
const url = await provider.authorizationUrl(state);
|
|
42
|
+
writeProviderState(response, provider.name, state);
|
|
43
|
+
return url;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Finish a redirect login on the provider's callback: consume the state
|
|
47
|
+
* cookie, reject a missing/forged/expired cookie, a cookie from another
|
|
48
|
+
* provider, a provider-reported error or a `state` mismatch; let the provider
|
|
49
|
+
* verify the callback; find-or-link the user; then log them in through
|
|
50
|
+
* `authService.completeLogin` — the same outcome as password login.
|
|
51
|
+
*
|
|
52
|
+
* @throws InvalidProviderCallbackError (400, EC009) for any callback rejection.
|
|
53
|
+
* @throws ProviderEmailNotVerifiedError (403, EC010) for an unlinked, unverified email.
|
|
54
|
+
* @throws AuthProviderSdkMissingError when the provider's SDK is not installed.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // GET /auth/google/callback
|
|
58
|
+
* const { user, tokens } = await completeProviderLogin(User, "google", request, response);
|
|
59
|
+
* authService.setAuthCookie(response, tokens.accessToken);
|
|
60
|
+
*/
|
|
61
|
+
async function completeProviderLogin(Model, providerName, request, response, deviceInfo) {
|
|
62
|
+
const provider = resolveAuthProvider(providerName);
|
|
63
|
+
const expected = takeProviderState(request, response);
|
|
64
|
+
if (!expected) throw new InvalidProviderCallbackError("missing-or-invalid-state-cookie");
|
|
65
|
+
if (expected.provider !== provider.name) throw new InvalidProviderCallbackError("state-cookie-for-another-provider");
|
|
66
|
+
const query = {
|
|
67
|
+
code: request.input("code"),
|
|
68
|
+
state: request.input("state"),
|
|
69
|
+
error: request.input("error")
|
|
70
|
+
};
|
|
71
|
+
if (query.error) throw new InvalidProviderCallbackError(`provider-error:${String(query.error)}`);
|
|
72
|
+
if (typeof query.state !== "string" || !safeEqual(query.state, expected.state)) throw new InvalidProviderCallbackError("state-mismatch");
|
|
73
|
+
const user = await resolveProviderUser(Model, await provider.handleCallback({
|
|
74
|
+
query,
|
|
75
|
+
expected: {
|
|
76
|
+
state: expected.state,
|
|
77
|
+
nonce: expected.nonce,
|
|
78
|
+
codeVerifier: expected.codeVerifier
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
return authService.completeLogin(user, deviceInfo);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { completeProviderLogin, resolveAuthProvider, startProviderLogin };
|
|
86
|
+
//# sourceMappingURL=provider-login.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-login.mjs","names":[],"sources":["../../../../../../../auth/src/providers/provider-login.ts"],"sourcesContent":["import type { ChildModel } from \"@warlock.js/cascade\";\nimport type { Request, Response } from \"@warlock.js/core\";\nimport type { AuthProvider } from \"../contracts/providers\";\nimport type { DeviceInfo, LoginResult } from \"../contracts/types\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { authConfig } from \"../services/auth-config\";\nimport { authService } from \"../services/auth.service\";\nimport { GoogleProvider } from \"./google-provider\";\nimport { safeEqual } from \"../utils/safe-equal\";\nimport { randomUrlSafe, takeProviderState, writeProviderState } from \"./provider-state-cookie\";\nimport { resolveProviderUser } from \"./resolve-provider-user\";\n\n/**\n * Resolve a provider by name: `google` from `auth.providers.google`, anything\n * else from `auth.providers.custom`. Throws naming the config key when absent.\n */\nexport function resolveAuthProvider(name: string): AuthProvider {\n if (name === \"google\") {\n const google = authConfig.providers.google();\n\n if (google) return new GoogleProvider(google);\n } else {\n const custom = authConfig.providers.custom(name);\n\n if (custom) return custom;\n }\n\n throw new Error(\n `@warlock.js/auth: no login provider \"${name}\" — configure \\`auth.providers.${\n name === \"google\" ? \"google\" : `custom.${name}`\n }\\`.`,\n );\n}\n\n/**\n * Start a redirect login: generate `state`, `nonce` and a PKCE verifier, store\n * them in a signed 10-minute cookie, and resolve the provider URL to redirect\n * the browser to.\n *\n * @example\n * // GET /auth/google\n * export const googleRedirect: RequestHandler = async (_request, response) =>\n * response.redirect(await startProviderLogin(response, \"google\"));\n */\nexport async function startProviderLogin(\n response: Response,\n providerName: string,\n): Promise<string> {\n const provider = resolveAuthProvider(providerName);\n\n const state = {\n state: randomUrlSafe(),\n nonce: randomUrlSafe(),\n codeVerifier: randomUrlSafe(),\n };\n\n const url = await provider.authorizationUrl(state);\n\n writeProviderState(response, provider.name, state);\n\n return url;\n}\n\n/**\n * Finish a redirect login on the provider's callback: consume the state\n * cookie, reject a missing/forged/expired cookie, a cookie from another\n * provider, a provider-reported error or a `state` mismatch; let the provider\n * verify the callback; find-or-link the user; then log them in through\n * `authService.completeLogin` — the same outcome as password login.\n *\n * @throws InvalidProviderCallbackError (400, EC009) for any callback rejection.\n * @throws ProviderEmailNotVerifiedError (403, EC010) for an unlinked, unverified email.\n * @throws AuthProviderSdkMissingError when the provider's SDK is not installed.\n *\n * @example\n * // GET /auth/google/callback\n * const { user, tokens } = await completeProviderLogin(User, \"google\", request, response);\n * authService.setAuthCookie(response, tokens.accessToken);\n */\nexport async function completeProviderLogin<T extends Auth>(\n Model: ChildModel<T>,\n providerName: string,\n request: Request,\n response: Response,\n deviceInfo?: DeviceInfo,\n): Promise<LoginResult<T>> {\n const provider = resolveAuthProvider(providerName);\n const expected = takeProviderState(request, response);\n\n if (!expected) {\n throw new InvalidProviderCallbackError(\"missing-or-invalid-state-cookie\");\n }\n\n if (expected.provider !== provider.name) {\n throw new InvalidProviderCallbackError(\"state-cookie-for-another-provider\");\n }\n\n const query: Record<string, unknown> = {\n code: request.input(\"code\"),\n state: request.input(\"state\"),\n error: request.input(\"error\"),\n };\n\n if (query.error) {\n throw new InvalidProviderCallbackError(`provider-error:${String(query.error)}`);\n }\n\n if (typeof query.state !== \"string\" || !safeEqual(query.state, expected.state)) {\n throw new InvalidProviderCallbackError(\"state-mismatch\");\n }\n\n const profile = await provider.handleCallback({\n query,\n expected: { state: expected.state, nonce: expected.nonce, codeVerifier: expected.codeVerifier },\n });\n\n const user = await resolveProviderUser(Model, profile);\n\n return authService.completeLogin(user, deviceInfo);\n}\n"],"mappings":";;;;;;;;;;;;;AAiBA,SAAgB,oBAAoB,MAA4B;CAC9D,IAAI,SAAS,UAAU;EACrB,MAAM,SAAS,WAAW,UAAU,OAAO;EAE3C,IAAI,QAAQ,OAAO,IAAI,eAAe,MAAM;CAC9C,OAAO;EACL,MAAM,SAAS,WAAW,UAAU,OAAO,IAAI;EAE/C,IAAI,QAAQ,OAAO;CACrB;CAEA,MAAM,IAAI,MACR,wCAAwC,KAAK,iCAC3C,SAAS,WAAW,WAAW,UAAU,OAC1C,IACH;AACF;;;;;;;;;;;AAYA,eAAsB,mBACpB,UACA,cACiB;CACjB,MAAM,WAAW,oBAAoB,YAAY;CAEjD,MAAM,QAAQ;EACZ,OAAO,cAAc;EACrB,OAAO,cAAc;EACrB,cAAc,cAAc;CAC9B;CAEA,MAAM,MAAM,MAAM,SAAS,iBAAiB,KAAK;CAEjD,mBAAmB,UAAU,SAAS,MAAM,KAAK;CAEjD,OAAO;AACT;;;;;;;;;;;;;;;;;AAkBA,eAAsB,sBACpB,OACA,cACA,SACA,UACA,YACyB;CACzB,MAAM,WAAW,oBAAoB,YAAY;CACjD,MAAM,WAAW,kBAAkB,SAAS,QAAQ;CAEpD,IAAI,CAAC,UACH,MAAM,IAAI,6BAA6B,iCAAiC;CAG1E,IAAI,SAAS,aAAa,SAAS,MACjC,MAAM,IAAI,6BAA6B,mCAAmC;CAG5E,MAAM,QAAiC;EACrC,MAAM,QAAQ,MAAM,MAAM;EAC1B,OAAO,QAAQ,MAAM,OAAO;EAC5B,OAAO,QAAQ,MAAM,OAAO;CAC9B;CAEA,IAAI,MAAM,OACR,MAAM,IAAI,6BAA6B,kBAAkB,OAAO,MAAM,KAAK,GAAG;CAGhF,IAAI,OAAO,MAAM,UAAU,YAAY,CAAC,UAAU,MAAM,OAAO,SAAS,KAAK,GAC3E,MAAM,IAAI,6BAA6B,gBAAgB;CAQzD,MAAM,OAAO,MAAM,oBAAoB,OAAO,MALxB,SAAS,eAAe;EAC5C;EACA,UAAU;GAAE,OAAO,SAAS;GAAO,OAAO,SAAS;GAAO,cAAc,SAAS;EAAa;CAChG,CAAC,CAEoD;CAErD,OAAO,YAAY,cAAc,MAAM,UAAU;AACnD"}
|