From af8a781818dc87e446a76138f3ae853d83f71667 Mon Sep 17 00:00:00 2001 From: Markus Ahlstrand Date: Tue, 17 Dec 2024 09:32:49 +0100 Subject: [PATCH 1/4] fix: removed old dbconnections --- package.json | 43 +- src/oauth-app.ts | 4 - src/routes/oauth2/dbconnections.ts | 176 -- test/integration/flows/code-flow.spec.ts | 3066 +++++++++++----------- test/integration/flows/social.spec.ts | 23 +- yarn.lock | 350 ++- 6 files changed, 1739 insertions(+), 1923 deletions(-) delete mode 100644 src/routes/oauth2/dbconnections.ts diff --git a/package.json b/package.json index e422e7dc5..21a161151 100644 --- a/package.json +++ b/package.json @@ -50,15 +50,15 @@ ] }, "dependencies": { - "@authhero/kysely-adapter": "0.25.2", - "@hono/zod-openapi": "0.16.2", + "@authhero/kysely-adapter": "0.25.3", + "@hono/zod-openapi": "0.18.3", "@peculiar/x509": "^1.12.3", "@planetscale/database": "1.19.0", - "arctic": "^2.3.0", - "authhero": "^0.25.2", + "arctic": "^2.3.2", + "authhero": "^0.26.0", "bcryptjs": "^2.4.3", - "fast-xml-parser": "^4.5.0", - "hono": "4.4.0", + "fast-xml-parser": "^4.5.1", + "hono": "4.6.14", "hono-openapi-middlewares": "^1.0.11", "kysely": "^0.27.5", "kysely-bun-sqlite": "^0.3.2", @@ -66,51 +66,52 @@ "liquidjs": "^10.19.0", "nanoid": "5.0.9", "oslo": "^1.2.1", - "playwright": "1.44.1", - "zod": "3.23.8" + "playwright": "1.49.1", + "zod": "3.24.1" }, "devDependencies": { "@ape-egg/tailwind-rows-columns": "1.0.2", - "@cloudflare/workers-types": "4.20241205.0", + "@cloudflare/workers-types": "4.20241216.0", "@eslint/compat": "^1.2.4", "@semantic-release/git": "10.0.1", "@types/bcryptjs": "2.4.6", "@types/better-sqlite3": "7.6.12", "@types/cookie": "1.0.0", "@types/jest-image-snapshot": "6.4.0", - "@types/node": "22.9.1", + "@types/node": "22.10.2", "@types/pako": "^2.0.3", "@types/service-worker-mock": "2.0.4", "@types/validator": "13.12.2", "autoprefixer": "^10.4.20", - "better-sqlite3": "11.5.0", + "better-sqlite3": "11.7.0", "classnames": "^2.5.1", - "dotenv": "16.4.5", - "eslint": "9.15.0", + "dotenv": "16.4.7", + "eslint": "9.17.0", "eslint-plugin-react": "^7.37.2", "husky": "9.1.7", - "i18next": "23.16.8", + "i18next": "24.1.2", "i18nexus-cli": "3.5.0", "jest-image-snapshot": "6.4.0", - "knip": "5.37.1", + "knip": "5.41.0", "mjml": "4.15.3", - "msw": "^2.6.5", + "msw": "^2.7.0", "postcss-cli": "^11.0.0", - "prettier": "3.3.3", + "prettier": "3.4.2", "prettier-plugin-tailwindcss": "0.6.9", "semantic-release": "24.2.0", "sort-json": "2.0.1", - "tailwindcss": "3.4.15", + "tailwindcss": "3.4.16", "typescript": "5.7.2", - "typescript-eslint": "^8.15.0", + "typescript-eslint": "^8.18.1", "validator": "13.12.0", "vitest": "2.1.8", "vitest-fetch-mock": "0.4.2", - "wrangler": "3.93.0" + "wrangler": "3.96.0" }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": [ "prettier --write" ] - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/oauth-app.ts b/src/oauth-app.ts index c96109358..c51632eff 100644 --- a/src/oauth-app.ts +++ b/src/oauth-app.ts @@ -5,8 +5,6 @@ import { CreateAuthParams } from "./app"; import { loginRoutes } from "./routes/universal-login/routes"; import { authorizeRoutes } from "./routes/oauth2/authorize"; import { callbackRoutes } from "./routes/oauth2/callback"; -import { dbConnectionRoutes } from "./routes/oauth2/dbconnections"; -import { passwordlessRoutes } from "./routes/oauth2/passwordless"; import { authenticateRoutes } from "./routes/oauth2/authenticate"; export default function create(params: CreateAuthParams) { @@ -21,8 +19,6 @@ export default function create(params: CreateAuthParams) { .route("/u", loginRoutes) .route("/authorize", authorizeRoutes) .route("/callback", callbackRoutes) - .route("/dbconnections", dbConnectionRoutes) - .route("/passwordless", passwordlessRoutes) .route("/co/authenticate", authenticateRoutes); oauthApp.doc("/spec", { diff --git a/src/routes/oauth2/dbconnections.ts b/src/routes/oauth2/dbconnections.ts deleted file mode 100644 index 33994b36d..000000000 --- a/src/routes/oauth2/dbconnections.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { HTTPException } from "hono/http-exception"; -import bcryptjs from "bcryptjs"; -import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; -import userIdGenerate from "../../utils/userIdGenerate"; -import { getClient } from "../../services/clients"; -import { getPrimaryUserByEmailAndProvider } from "../../utils/users"; -import { Env, Var } from "../../types"; -import { sendEmailVerificationEmail } from "../../authentication-flows/passwordless"; -import validatePassword from "../../utils/validatePassword"; -import { createLogMessage } from "../../utils/create-log-message"; -import { requestPasswordReset } from "../../authentication-flows/password"; -import { UNIVERSAL_AUTH_SESSION_EXPIRES_IN_SECONDS } from "../../constants"; -import { AuthParams, LogTypes } from "authhero"; -import { getClientInfo } from "../../utils/client-info"; - -export const dbConnectionRoutes = new OpenAPIHono<{ - Bindings: Env; - Variables: Var; -}>() - // -------------------------------- - // POST /dbconnections/signup - // -------------------------------- - .openapi( - createRoute({ - tags: ["dbconnections"], - method: "post", - path: "/signup", - request: { - body: { - content: { - "application/json": { - schema: z.object({ - client_id: z.string(), - connection: z.literal("Username-Password-Authentication"), - email: z.string().transform((u) => u.toLowerCase()), - password: z.string(), - }), - }, - }, - }, - }, - responses: { - 200: { - content: { - "application/json": { - schema: z.object({ - _id: z.string(), - email: z.string(), - email_verified: z.boolean(), - app_metadata: z.object({}), - user_metadata: z.object({}), - }), - }, - }, - description: "Created user", - }, - }, - }), - async (ctx) => { - const { email, password, client_id } = ctx.req.valid("json"); - - // auth0 returns a detailed JSON response with the way the password does match the strength rules - if (!validatePassword(password)) { - throw new HTTPException(400, { - message: "Password does not meet the requirements", - }); - } - - const client = await getClient(ctx.env, client_id); - ctx.set("client_id", client.id); - - const existingUser = await getPrimaryUserByEmailAndProvider({ - userAdapter: ctx.env.data.users, - tenant_id: client.tenant.id, - email, - provider: "auth2", - }); - - if (existingUser) { - // Auth0 doesn't inform that the user already exists - throw new HTTPException(400, { message: "Invalid sign up" }); - } - - const newUser = await ctx.env.data.users.create(client.tenant.id, { - user_id: `auth2|${userIdGenerate()}`, - email, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - email_verified: false, - provider: "auth2", - connection: "Username-Password-Authentication", - is_social: false, - login_count: 0, - }); - - ctx.set("userId", newUser.user_id); - ctx.set("userName", newUser.email); - ctx.set("connection", newUser.connection); - - // Store the password - await ctx.env.data.passwords.create(client.tenant.id, { - user_id: newUser.user_id, - password: bcryptjs.hashSync(password, 10), - algorithm: "bcrypt", - }); - - await sendEmailVerificationEmail({ - ctx, - client, - user: newUser, - }); - - const log = createLogMessage(ctx, { - type: LogTypes.SUCCESS_SIGNUP, - description: "Successful signup", - }); - await ctx.env.data.logs.create(client.tenant.id, log); - - return ctx.json({ - _id: newUser.user_id, - email: newUser.email, - email_verified: false, - app_metadata: {}, - user_metadata: {}, - }); - }, - ) // -------------------------------- - // POST /dbconnections/change_password - // -------------------------------- - .openapi( - createRoute({ - tags: ["dbconnections"], - method: "post", - path: "/change_password", - request: { - body: { - content: { - "application/json": { - schema: z.object({ - client_id: z.string(), - connection: z.literal("Username-Password-Authentication"), - email: z.string().transform((u) => u.toLowerCase()), - }), - }, - }, - }, - }, - responses: { - 200: { - description: "Redirect to the client's redirect uri", - }, - }, - }), - async (ctx) => { - const { email, client_id } = ctx.req.valid("json"); - - const client = await getClient(ctx.env, client_id); - - const authParams: AuthParams = { - client_id: client_id, - username: email, - }; - - const loginSession = await ctx.env.data.logins.create(client.tenant.id, { - expires_at: new Date( - Date.now() + UNIVERSAL_AUTH_SESSION_EXPIRES_IN_SECONDS * 1000, - ).toISOString(), - authParams, - ...getClientInfo(ctx.req), - }); - - await requestPasswordReset(ctx, client, email, loginSession.login_id); - - return ctx.html("We've just sent you an email to reset your password."); - }, - ); diff --git a/test/integration/flows/code-flow.spec.ts b/test/integration/flows/code-flow.spec.ts index 0ba184370..9b5f11a46 100644 --- a/test/integration/flows/code-flow.spec.ts +++ b/test/integration/flows/code-flow.spec.ts @@ -1,1533 +1,1533 @@ -import { describe, it, expect } from "vitest"; -import { parseJwt } from "../../../src/utils/parse-jwt"; -import { UserResponse } from "../../../src/types/auth0"; -import { doSilentAuthRequestAndReturnTokens } from "../helpers/silent-auth"; -import { testClient } from "hono/testing"; -import { getAdminToken } from "../helpers/token"; -import { getTestServer } from "../helpers/test-server"; -import { EmailOptions } from "../../../src/services/email/EmailOptions"; -import { snapshotEmail } from "../helpers/playwrightSnapshots"; -import { AuthorizationResponseType, Log } from "authhero"; - -const AUTH_PARAMS = { - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: "state", -}; - -function getOTP(email: EmailOptions) { - const codeEmailBody = email.content[0].value; - // this ignores number prefixed by hashes so we don't match CSS colours - const otps = codeEmailBody.match(/(?!#).[0-9]{6}/g)!; - const otp = otps[0].slice(1); - - const to = email.to[0].email; - - return { otp, to }; -} - -describe("code-flow", () => { - it("should create new user when email does not exist", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ----------------- - // Doing a new signup here, so expect this email not to exist - // ----------------- - const resInitialQuery = await managementClient["users-by-email"].$get( - { - query: { - email: "test@example.com", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - const results = await resInitialQuery.json(); - expect(results).toEqual([]); - - // ----------------- - // Start the passwordless flow - // ----------------- - const response = await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "test@example.com", - // can be code or link - send: "code", - }, - }); - - if (response.status !== 200) { - throw new Error(await response.text()); - } - - const { otp } = getOTP(emails[0]); - - await snapshotEmail(emails[0], true); - - const { - logs: [clsLog], - } = await env.data.logs.list("tenantId", { - page: 0, - per_page: 100, - include_totals: true, - }); - expect(clsLog).toMatchObject({ - type: "cls", - tenant_id: "tenantId", - user_id: "", // this is correct. Auth0 does not tie this log to a user account - description: "test@example.com", // we only know which user it is by looking at the description field - }); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "test@example.com", - }, - }); - - if (authenticateResponse.status !== 200) { - throw new Error( - `Failed to authenticate with status: ${ - authenticateResponse.status - } and message: ${await response.text()}`, - ); - } - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - const query = { - ...AUTH_PARAMS, - auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - referrer: "https://login.example.com", - realm: "email", - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get({ - query, - }); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - expect(redirectUri.hostname).toBe("login.example.com"); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - expect(searchParams.get("state")).toBe("state"); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe("openid profile email"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("test@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - - const { logs } = await env.data.logs.list("tenantId", { - page: 0, - per_page: 100, - include_totals: true, - }); - - expect(logs.length).toBe(2); - const log = logs.find((log: Log) => log.type === "scoa"); - - expect(log).toMatchObject({ - type: "scoa", - tenant_id: "tenantId", - user_id: accessTokenPayload.sub, - user_name: "test@example.com", - }); - - // now check silent auth works when logged in with code---------------------------------------- - const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload.sub).toContain("email|"); - expect(silentAuthIdTokenPayload).toMatchObject({ - aud: "clientId", - name: "test@example.com", - email: "test@example.com", - email_verified: true, - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - iss: "https://example.com/", - }); - - // ---------------------------- - // Now log in (previous flow was signup) - // ---------------------------- - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "test@example.com", - send: "code", - }, - }); - - const { otp: otpLogin } = getOTP(emails[1]); - - const authRes2 = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp: otpLogin, - realm: "email", - username: "test@example.com", - }, - }); - - const { login_ticket: loginTicket2 } = (await authRes2.json()) as { - login_ticket: string; - }; - - const tokenRes2 = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket: loginTicket2, - ...AUTH_PARAMS, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - // ---------------------------- - // Now silent auth again - confirms that logging in works - // ---------------------------- - const setCookiesHeader2 = tokenRes2.headers.get("set-cookie")!; - const { idToken: silentAuthIdTokenPayload2 } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader2, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload2.sub).toEqual(silentAuthIdTokenPayload.sub); - expect(silentAuthIdTokenPayload2).toMatchObject({ - aud: "clientId", - name: "test@example.com", - email: "test@example.com", - email_verified: true, - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - iss: "https://example.com/", - }); - }); - it("is an existing primary user", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ----------------- - // Create the user to log in with the code - // ----------------- - env.data.users.create("tenantId", { - user_id: "email|userId2", - email: "bar@example.com", - email_verified: true, - name: "", - nickname: "", - picture: "https://example.com/foo.png", - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }); - - const resInitialQuery = await managementClient["users-by-email"].$get( - { - query: { - email: "bar@example.com", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - expect(resInitialQuery.status).toBe(200); - - // ----------------- - // Start the passwordless flow - // ----------------- - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "bar@example.com", - send: "code", - }, - }); - - const { otp } = getOTP(emails[0]); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "bar@example.com", - }, - }); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - const query = { - ...AUTH_PARAMS, - auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - referrer: "https://login.example.com", - realm: "email", - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get({ - query, - }); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.sub).toBe("email|userId2"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("bar@example.com"); - - // now check silent auth works when logged in with code---------------------------------------- - const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload.sub).toBe("email|userId2"); - }); - it("is an existing linked user", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------------- - // Create the linked user to log in with the magic link - // ----------------- - env.data.users.create("tenantId", { - user_id: "email|userId2", - // same email address as existing primary user... but this isn't needed - // do we need more tests where this is different? In case I've taken shortcuts looking up by email address... - email: "foo@example.com", - email_verified: true, - name: "", - nickname: "", - picture: "https://example.com/foo.png", - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - linked_to: "auth2|userId", - }); - - // ----------------- - // Start the passwordless flow - // ----------------- - await oauthClient.passwordless.start.$post( - { - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "code", - }, - }, - { - headers: { - "content-type": "application/json", - }, - }, - ); - - const { otp } = getOTP(emails[0]); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "foo@example.com", - }, - }); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - ...AUTH_PARAMS, - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - // this shows we are getting the primary user - expect(accessTokenPayload.sub).toBe("auth2|userId"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("foo@example.com"); - - // now check silent auth works when logged in with code---------------------------------------- - const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - // getting the primary user back again - expect(silentAuthIdTokenPayload.sub).toBe("auth2|userId"); - }); - - it("should return existing username-primary account when logging in with new code sign on with same email address", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - const nonce = "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM"; - const redirect_uri = "https://login.example.com/callback"; - const response_type = AuthorizationResponseType.TOKEN_ID_TOKEN; - const scope = "openid profile email"; - const state = "state"; - - await oauthClient.passwordless.start.$post({ - json: { - authParams: { - nonce, - redirect_uri, - response_type, - scope, - state, - }, - client_id: "clientId", - connection: "email", - // this email already exists as a Username-Password-Authentication user - email: "foo@example.com", - send: "link", - }, - }); - - const { otp } = getOTP(emails[0]); - - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "foo@example.com", - }, - }); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - nonce, - redirect_uri, - response_type, - scope, - state, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - const accessTokenPayload = parseJwt(accessToken!); - - // this is the id of the primary account - expect(accessTokenPayload.sub).toBe("auth2|userId"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - - expect(idTokenPayload.sub).toBe("auth2|userId"); - - // ---------------------------- - // now check the primary user has a new 'email' connection identity - // ---------------------------- - const primaryUserRes = await managementClient.users[":user_id"].$get( - { - param: { - user_id: "auth2|userId", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - - const primaryUser = (await primaryUserRes.json()) as UserResponse; - - expect(primaryUser.identities[1]).toMatchObject({ - connection: "email", - provider: "email", - isSocial: false, - profileData: { email: "foo@example.com", email_verified: true }, - }); - - // ---------------------------- - // now check silent auth works when logged in with code - // ---------------------------- - - const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader, - oauthClient, - nonce, - "clientId", - ); - - // this is the id of the primary account - expect(silentAuthIdTokenPayload.sub).toBe("auth2|userId"); - - expect(silentAuthIdTokenPayload).toMatchObject({ - aud: "clientId", - email: "foo@example.com", - email_verified: true, - iss: "https://example.com/", - name: "Åkesson Þorsteinsson", - nickname: "Åkesson Þorsteinsson", - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - picture: "https://example.com/foo.png", - }); - - // ---------------------------- - // now log in again with the same email and code user - // ---------------------------- - - await oauthClient.passwordless.start.$post({ - json: { - authParams: { - nonce: "nonce", - redirect_uri, - response_type, - scope, - state, - }, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "link", - }, - }); - - const { otp: otp2 } = getOTP(emails[1]); - - const authenticateResponse2 = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp: otp2, - realm: "email", - username: "foo@example.com", - }, - }); - - const { login_ticket: loginTicket2 } = - (await authenticateResponse2.json()) as { - login_ticket: string; - }; - const tokenResponse2 = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket: loginTicket2, - nonce: "nonce", - redirect_uri, - response_type, - scope, - state, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - const accessToken2 = parseJwt( - new URLSearchParams( - tokenResponse2.headers.get("location")!.split("#")[1]!, - ).get("access_token")!, - ); - - // this is the id of the primary account - expect(accessToken2.sub).toBe("auth2|userId"); - - // ---------------------------- - // now check silent auth again! - // ---------------------------- - const setCookiesHeader2 = tokenResponse2.headers.get("set-cookie")!; - const { idToken: silentAuthIdTokenPayload2 } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader2, - oauthClient, - nonce, - "clientId", - ); - // second time round make sure we get the primary userid again - expect(silentAuthIdTokenPayload2.sub).toBe("auth2|userId"); - }); - - describe("most complex linking flow I can think of", () => { - it("should follow linked_to chain when logging in with new code user with same email address as existing username-password user THAT IS linked to a code user with a different email address", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ----------------- - // create code user - the base user - // ----------------- - - await env.data.users.create("tenantId", { - user_id: "email|the-base-user", - email: "the-base-user@example.com", - email_verified: true, - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }); - - // ----------------- - // create username-password user with different email address and link to the above user - // ----------------- - - await env.data.users.create("tenantId", { - user_id: "auth2|the-auth2-same-email-user", - email: "same-email@example.com", - email_verified: true, - login_count: 0, - provider: "auth2", - connection: "Username-Password-Authentication", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - linked_to: "email|the-base-user", - }); - - // ----------------- - // sanity check these users are linked - // ----------------- - - const baseUserRes = await managementClient.users[":user_id"].$get( - { - param: { - user_id: "email|the-base-user", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - - const baseUser = (await baseUserRes.json()) as UserResponse; - - expect(baseUser.identities).toEqual([ - { - connection: "email", - provider: "email", - user_id: "the-base-user", - isSocial: false, - }, - { - connection: "Username-Password-Authentication", - provider: "auth2", - user_id: "the-auth2-same-email-user", - isSocial: false, - profileData: { - email: "same-email@example.com", - email_verified: true, - }, - }, - ]); - - // ----------------- - // Now do a new passwordless flow with a new user with email same-email@example.com - // ----------------- - - const passwordlessStartRes = await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "same-email@example.com", - send: "code", - }, - }); - expect(passwordlessStartRes.status).toBe(200); - - const { otp } = getOTP(emails[0]); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "same-email@example.com", - }, - }); - expect(authenticateResponse.status).toBe(200); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - ...AUTH_PARAMS, - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - const accessToken = searchParams.get("access_token"); - const accessTokenPayload = parseJwt(accessToken!); - - // this proves that we are following the linked user chain - expect(accessTokenPayload.sub).toBe("email|the-base-user"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - // this proves that we are following the linked user chain - expect(idTokenPayload.email).toBe("the-base-user@example.com"); - - // now check silent auth works when logged in with code---------------------------------------- - const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - setCookiesHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - // this proves the account linking chain is still working - expect(silentAuthIdTokenPayload.sub).toBe("email|the-base-user"); - - //------------------------------------------------------------------------------------------------ - // fetch the base user again now and check we have THREE identities in there - //------------------------------------------------------------------------------------------------ - - const baseUserRes2 = await managementClient.users[":user_id"].$get( - { - param: { - user_id: "email|the-base-user", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - - const baseUser2 = (await baseUserRes2.json()) as UserResponse; - - expect(baseUser2.identities).toEqual([ - { - connection: "email", - provider: "email", - user_id: "the-base-user", - isSocial: false, - }, - { - connection: "Username-Password-Authentication", - provider: "auth2", - user_id: "the-auth2-same-email-user", - isSocial: false, - profileData: { - email: "same-email@example.com", - email_verified: true, - }, - }, - { - connection: "email", - isSocial: false, - profileData: { - email: "same-email@example.com", - email_verified: true, - }, - provider: "email", - user_id: baseUser2.identities[2].user_id, - }, - ]); - }); - }); - - it.skip("should only allow a code to be used once", async () => { - const AUTH_PARAMS = { - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: "state", - }; - - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "code", - }, - }); - - const { otp } = getOTP(emails[0]); - - const authRes = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "foo@example.com", - }, - }); - expect(authRes.status).toBe(200); - - // now try to use the same code again - const authRes2 = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "foo@example.com", - }, - }); - - expect(authRes2.status).toBe(403); - // this message isn't exactly true! We could check what auth0 does - expect(await authRes2.json()).toEqual({ - error: "access_denied", - error_description: "Wrong email or verification code.", - }); - }); - - it("should not accept an invalid code", async () => { - const AUTH_PARAMS = { - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: "state", - }; - - const { oauthApp, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - await oauthClient.passwordless.start.$post( - { - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "code", - }, - }, - { - headers: { - "content-type": "application/json", - }, - }, - ); - - const BAD_CODE = "123456"; - - const authRes = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp: BAD_CODE, - realm: "email", - username: "foo@example.com", - }, - }); - - expect(authRes.status).toBe(403); - }); - - it("should be case insensitive with email address", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ------------------------- - // Create new email user - all lower case email - // ------------------------- - const createUserResponse1 = await managementClient.users.$post( - { - json: { - email: "john-doe@example.com", - connection: "email", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - - expect(createUserResponse1.status).toBe(201); - const newUser1 = (await createUserResponse1.json()) as UserResponse; - expect(newUser1.email).toBe("john-doe@example.com"); - - const AUTH_PARAMS = { - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: "state", - }; - - // ----------------- - // Sign in with same user passwordless - // ----------------- - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - // do we want two tests? one for the username uppercase one for the domain? - email: "JOHN-DOE@example.com", - send: "code", - }, - }); - - expect(emails.length).toBe(1); - const { otp, to } = getOTP(emails[0]); - expect(to).toBe("john-doe@example.com"); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "JOHN-DOE@example.com", - }, - }); - - if (authenticateResponse.status !== 200) { - throw new Error(await authenticateResponse.text()); - } - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - const query = { - ...AUTH_PARAMS, - auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - referrer: "https://login.example.com", - realm: "email", - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get({ - query, - }); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - expect(redirectUri.hostname).toBe("login.example.com"); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - expect(searchParams.get("state")).toBe("state"); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.sub).toBe(newUser1.user_id); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("john-doe@example.com"); - }); - - it("should store new user email in lowercase", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const AUTH_PARAMS = { - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: "state", - }; - - // ----------------- - // New passwordless sign up all uppercase - login2 would stop this... What does auth0.js do? CHECK! - // ----------------- - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "JOHN-DOE@EXAMPLE.COM", - send: "code", - }, - }); - - const { otp } = getOTP(emails[0]); - expect(otp).toBeTypeOf("string"); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - // use lowercase here... TBD - username: "john-doe@example.com", - }, - }); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - ...AUTH_PARAMS, - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - const redirectUri = new URL(tokenResponse.headers.get("location")!); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - const accessToken = searchParams.get("access_token"); - - const sub = parseJwt(accessToken!).sub; - - // this means we have created the user - expect(tokenResponse.status).toBe(302); - - // Now check in database we are storing in lower case - - const newLowercaseUser = await env.data.users.get("tenantId", sub); - - expect(newLowercaseUser!.email).toBe("john-doe@example.com"); - }); - - // TO TEST - // - using expired codes? how can we fast-forward time with wrangler... - // - more linked accounts - // more basic error testing e.g. - // - do not allow code from a different account: we should be fine without this but I can see a way we could mess this up! - - describe("edge cases", () => { - it("should login correctly for a code account linked to another account with a different email, when a password account has been registered but not verified", async () => { - // create a new user with a password - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ----------------- - // user fixtures - // ----------------- - - // create new password user - await env.data.users.create("tenantId", { - user_id: "auth2|base-user", - email: "base-user@example.com", - email_verified: true, - login_count: 0, - provider: "auth2", - connection: "Username-Password-Authentication", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }); - // create new code user and link this to the password user - await env.data.users.create("tenantId", { - user_id: "auth2|code-user", - email: "code-user@example.com", - email_verified: true, - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - linked_to: "auth2|base-user", - }); - - // sanity check - get base user and check identities - const baseUserRes = await managementClient.users[":user_id"].$get( - { - param: { - user_id: "auth2|base-user", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - expect(baseUserRes.status).toBe(200); - const baseUser = (await baseUserRes.json()) as UserResponse; - expect(baseUser.identities).toEqual([ - { - connection: "Username-Password-Authentication", - isSocial: false, - provider: "auth2", - user_id: "base-user", - }, - { - connection: "email", - isSocial: false, - profileData: { - email: "code-user@example.com", - email_verified: true, - }, - provider: "email", - user_id: "code-user", - }, - ]); - - // ----------------- - // Now start password sign up with same code-user@example.com email - // I'm seeing if this affects the code user with the same email address - // ----------------- - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "code-user@example.com", - password: "Password1234!", - }, - }); - - expect(createUserResponse.status).toBe(200); - - //----------------- - // now try and sign in with code-user@example.com code flow - // I'm testing that the unlinked password user with the same email address does not affect this - // ----------------- - - const response = await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "code-user@example.com", - send: "code", - }, - }); - - expect(response.status).toBe(200); - - // first email is email validation from sign up above - const { otp } = getOTP(emails[1]); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "code-user@example.com", - }, - }); - - if (authenticateResponse.status !== 200) { - throw new Error( - `Failed to authenticate with status: ${ - authenticateResponse.status - } and message: ${await response.text()}`, - ); - } - - expect(authenticateResponse.status).toBe(200); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - ...AUTH_PARAMS, - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - const accessToken = searchParams.get("access_token"); - const accessTokenPayload = parseJwt(accessToken!); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - - // these prove that we are getting the code account's primary account! - expect(accessTokenPayload.sub).toBe("auth2|base-user"); - expect(idTokenPayload.email).toBe("base-user@example.com"); - }); - - it("should ignore un-verified password account when signing up with code account", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------------- - // signup new user - // ----------------- - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "same-user-signin@example.com", - password: "Password1234!", - }, - }); - - expect(createUserResponse.status).toBe(200); - - const unverifiedPasswordUser = await createUserResponse.json(); - - //----------------- - // sign up new code user that has same email address - //----------------- - const response = await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "same-user-signin@example.com", - send: "code", - }, - }); - - if (response.status !== 200) { - throw new Error(await response.text()); - } - - // first email will be email verification - const { otp } = getOTP(emails[1]); - - // Authenticate using the code - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "same-user-signin@example.com", - }, - }); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - ...AUTH_PARAMS, - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - realm: "email", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); - expect(idTokenPayload.email_verified).toBe(true); - }); - - // tickets are used by a few flows so this probably should not be here - it("should only allow a ticket to be used once", async () => { - const AUTH_PARAMS = { - nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: "state", - }; - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "code", - }, - }); - const { otp } = getOTP(emails[0]); - - const authenticateResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", - otp, - realm: "email", - username: "foo@example.com", - }, - }); - expect(authenticateResponse.status).toBe(200); - - const { login_ticket } = (await authenticateResponse.json()) as { - login_ticket: string; - }; - - // ----------------- - // Trade the ticket for token once so it is used - // ----------------- - - const query = { - ...AUTH_PARAMS, - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - realm: "email", - }; - - const tokenResponse = await oauthClient.authorize.$get( - { - query, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - - // ----------------- - // Now try trading ticket again and it should not work - // ----------------- - const rejectedSecondTicketUsageRes = await oauthClient.authorize.$get({ - query, - }); - - expect(rejectedSecondTicketUsageRes.status).toBe(403); - expect(await rejectedSecondTicketUsageRes.text()).toBe( - "Ticket not found", - ); - }); - }); -}); +// import { describe, it, expect } from "vitest"; +// import { parseJwt } from "../../../src/utils/parse-jwt"; +// import { UserResponse } from "../../../src/types/auth0"; +// import { doSilentAuthRequestAndReturnTokens } from "../helpers/silent-auth"; +// import { testClient } from "hono/testing"; +// import { getAdminToken } from "../helpers/token"; +// import { getTestServer } from "../helpers/test-server"; +// import { EmailOptions } from "../../../src/services/email/EmailOptions"; +// import { snapshotEmail } from "../helpers/playwrightSnapshots"; +// import { AuthorizationResponseType, Log } from "authhero"; + +// const AUTH_PARAMS = { +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// redirect_uri: "https://login.example.com/callback", +// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, +// scope: "openid profile email", +// state: "state", +// }; + +// function getOTP(email: EmailOptions) { +// const codeEmailBody = email.content[0].value; +// // this ignores number prefixed by hashes so we don't match CSS colours +// const otps = codeEmailBody.match(/(?!#).[0-9]{6}/g)!; +// const otp = otps[0].slice(1); + +// const to = email.to[0].email; + +// return { otp, to }; +// } + +// describe("code-flow", () => { +// it("should create new user when email does not exist", async () => { +// const token = await getAdminToken(); +// const { managementApp, oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); +// const managementClient = testClient(managementApp, env); + +// // ----------------- +// // Doing a new signup here, so expect this email not to exist +// // ----------------- +// const resInitialQuery = await managementClient["users-by-email"].$get( +// { +// query: { +// email: "test@example.com", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); +// const results = await resInitialQuery.json(); +// expect(results).toEqual([]); + +// // ----------------- +// // Start the passwordless flow +// // ----------------- +// const response = await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "test@example.com", +// // can be code or link +// send: "code", +// }, +// }); + +// if (response.status !== 200) { +// throw new Error(await response.text()); +// } + +// const { otp } = getOTP(emails[0]); + +// await snapshotEmail(emails[0], true); + +// const { +// logs: [clsLog], +// } = await env.data.logs.list("tenantId", { +// page: 0, +// per_page: 100, +// include_totals: true, +// }); +// expect(clsLog).toMatchObject({ +// type: "cls", +// tenant_id: "tenantId", +// user_id: "", // this is correct. Auth0 does not tie this log to a user account +// description: "test@example.com", // we only know which user it is by looking at the description field +// }); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "test@example.com", +// }, +// }); + +// if (authenticateResponse.status !== 200) { +// throw new Error( +// `Failed to authenticate with status: ${ +// authenticateResponse.status +// } and message: ${await response.text()}`, +// ); +// } + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// const query = { +// ...AUTH_PARAMS, +// auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// referrer: "https://login.example.com", +// realm: "email", +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get({ +// query, +// }); + +// expect(tokenResponse.status).toBe(302); +// expect(await tokenResponse.text()).toBe("Redirecting"); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); + +// expect(redirectUri.hostname).toBe("login.example.com"); + +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); + +// expect(searchParams.get("state")).toBe("state"); + +// const accessToken = searchParams.get("access_token"); + +// const accessTokenPayload = parseJwt(accessToken!); +// expect(accessTokenPayload.aud).toBe("default"); +// expect(accessTokenPayload.iss).toBe("https://example.com/"); +// expect(accessTokenPayload.scope).toBe("openid profile email"); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); +// expect(idTokenPayload.email).toBe("test@example.com"); +// expect(idTokenPayload.aud).toBe("clientId"); + +// const { logs } = await env.data.logs.list("tenantId", { +// page: 0, +// per_page: 100, +// include_totals: true, +// }); + +// expect(logs.length).toBe(2); +// const log = logs.find((log: Log) => log.type === "scoa"); + +// expect(log).toMatchObject({ +// type: "scoa", +// tenant_id: "tenantId", +// user_id: accessTokenPayload.sub, +// user_name: "test@example.com", +// }); + +// // now check silent auth works when logged in with code---------------------------------------- +// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; + +// const { idToken: silentAuthIdTokenPayload } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader, +// oauthClient, +// AUTH_PARAMS.nonce, +// "clientId", +// ); + +// expect(silentAuthIdTokenPayload.sub).toContain("email|"); +// expect(silentAuthIdTokenPayload).toMatchObject({ +// aud: "clientId", +// name: "test@example.com", +// email: "test@example.com", +// email_verified: true, +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// iss: "https://example.com/", +// }); + +// // ---------------------------- +// // Now log in (previous flow was signup) +// // ---------------------------- +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "test@example.com", +// send: "code", +// }, +// }); + +// const { otp: otpLogin } = getOTP(emails[1]); + +// const authRes2 = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp: otpLogin, +// realm: "email", +// username: "test@example.com", +// }, +// }); + +// const { login_ticket: loginTicket2 } = (await authRes2.json()) as { +// login_ticket: string; +// }; + +// const tokenRes2 = await oauthClient.authorize.$get( +// { +// query: { +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket: loginTicket2, +// ...AUTH_PARAMS, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// // ---------------------------- +// // Now silent auth again - confirms that logging in works +// // ---------------------------- +// const setCookiesHeader2 = tokenRes2.headers.get("set-cookie")!; +// const { idToken: silentAuthIdTokenPayload2 } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader2, +// oauthClient, +// AUTH_PARAMS.nonce, +// "clientId", +// ); + +// expect(silentAuthIdTokenPayload2.sub).toEqual(silentAuthIdTokenPayload.sub); +// expect(silentAuthIdTokenPayload2).toMatchObject({ +// aud: "clientId", +// name: "test@example.com", +// email: "test@example.com", +// email_verified: true, +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// iss: "https://example.com/", +// }); +// }); +// it("is an existing primary user", async () => { +// const token = await getAdminToken(); +// const { managementApp, oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); +// const managementClient = testClient(managementApp, env); + +// // ----------------- +// // Create the user to log in with the code +// // ----------------- +// env.data.users.create("tenantId", { +// user_id: "email|userId2", +// email: "bar@example.com", +// email_verified: true, +// name: "", +// nickname: "", +// picture: "https://example.com/foo.png", +// login_count: 0, +// provider: "email", +// connection: "email", +// is_social: false, +// created_at: new Date().toISOString(), +// updated_at: new Date().toISOString(), +// }); + +// const resInitialQuery = await managementClient["users-by-email"].$get( +// { +// query: { +// email: "bar@example.com", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); +// expect(resInitialQuery.status).toBe(200); + +// // ----------------- +// // Start the passwordless flow +// // ----------------- +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "bar@example.com", +// send: "code", +// }, +// }); + +// const { otp } = getOTP(emails[0]); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "bar@example.com", +// }, +// }); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// const query = { +// ...AUTH_PARAMS, +// auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// referrer: "https://login.example.com", +// realm: "email", +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get({ +// query, +// }); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); + +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); + +// const accessToken = searchParams.get("access_token"); + +// const accessTokenPayload = parseJwt(accessToken!); +// expect(accessTokenPayload.sub).toBe("email|userId2"); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); +// expect(idTokenPayload.email).toBe("bar@example.com"); + +// // now check silent auth works when logged in with code---------------------------------------- +// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; + +// const { idToken: silentAuthIdTokenPayload } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader, +// oauthClient, +// AUTH_PARAMS.nonce, +// "clientId", +// ); + +// expect(silentAuthIdTokenPayload.sub).toBe("email|userId2"); +// }); +// it("is an existing linked user", async () => { +// const { oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); + +// // ----------------- +// // Create the linked user to log in with the magic link +// // ----------------- +// env.data.users.create("tenantId", { +// user_id: "email|userId2", +// // same email address as existing primary user... but this isn't needed +// // do we need more tests where this is different? In case I've taken shortcuts looking up by email address... +// email: "foo@example.com", +// email_verified: true, +// name: "", +// nickname: "", +// picture: "https://example.com/foo.png", +// login_count: 0, +// provider: "email", +// connection: "email", +// is_social: false, +// created_at: new Date().toISOString(), +// updated_at: new Date().toISOString(), +// linked_to: "auth2|userId", +// }); + +// // ----------------- +// // Start the passwordless flow +// // ----------------- +// await oauthClient.passwordless.start.$post( +// { +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "foo@example.com", +// send: "code", +// }, +// }, +// { +// headers: { +// "content-type": "application/json", +// }, +// }, +// ); + +// const { otp } = getOTP(emails[0]); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "foo@example.com", +// }, +// }); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query: { +// ...AUTH_PARAMS, +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); + +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); + +// const accessToken = searchParams.get("access_token"); + +// const accessTokenPayload = parseJwt(accessToken!); +// // this shows we are getting the primary user +// expect(accessTokenPayload.sub).toBe("auth2|userId"); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); +// expect(idTokenPayload.email).toBe("foo@example.com"); + +// // now check silent auth works when logged in with code---------------------------------------- +// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; + +// const { idToken: silentAuthIdTokenPayload } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader, +// oauthClient, +// AUTH_PARAMS.nonce, +// "clientId", +// ); + +// // getting the primary user back again +// expect(silentAuthIdTokenPayload.sub).toBe("auth2|userId"); +// }); + +// it("should return existing username-primary account when logging in with new code sign on with same email address", async () => { +// const token = await getAdminToken(); +// const { managementApp, oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); +// const managementClient = testClient(managementApp, env); + +// const nonce = "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM"; +// const redirect_uri = "https://login.example.com/callback"; +// const response_type = AuthorizationResponseType.TOKEN_ID_TOKEN; +// const scope = "openid profile email"; +// const state = "state"; + +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: { +// nonce, +// redirect_uri, +// response_type, +// scope, +// state, +// }, +// client_id: "clientId", +// connection: "email", +// // this email already exists as a Username-Password-Authentication user +// email: "foo@example.com", +// send: "link", +// }, +// }); + +// const { otp } = getOTP(emails[0]); + +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "foo@example.com", +// }, +// }); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query: { +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// nonce, +// redirect_uri, +// response_type, +// scope, +// state, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); + +// const accessToken = searchParams.get("access_token"); +// const accessTokenPayload = parseJwt(accessToken!); + +// // this is the id of the primary account +// expect(accessTokenPayload.sub).toBe("auth2|userId"); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); + +// expect(idTokenPayload.sub).toBe("auth2|userId"); + +// // ---------------------------- +// // now check the primary user has a new 'email' connection identity +// // ---------------------------- +// const primaryUserRes = await managementClient.users[":user_id"].$get( +// { +// param: { +// user_id: "auth2|userId", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); + +// const primaryUser = (await primaryUserRes.json()) as UserResponse; + +// expect(primaryUser.identities[1]).toMatchObject({ +// connection: "email", +// provider: "email", +// isSocial: false, +// profileData: { email: "foo@example.com", email_verified: true }, +// }); + +// // ---------------------------- +// // now check silent auth works when logged in with code +// // ---------------------------- + +// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; +// const { idToken: silentAuthIdTokenPayload } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader, +// oauthClient, +// nonce, +// "clientId", +// ); + +// // this is the id of the primary account +// expect(silentAuthIdTokenPayload.sub).toBe("auth2|userId"); + +// expect(silentAuthIdTokenPayload).toMatchObject({ +// aud: "clientId", +// email: "foo@example.com", +// email_verified: true, +// iss: "https://example.com/", +// name: "Åkesson Þorsteinsson", +// nickname: "Åkesson Þorsteinsson", +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// picture: "https://example.com/foo.png", +// }); + +// // ---------------------------- +// // now log in again with the same email and code user +// // ---------------------------- + +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: { +// nonce: "nonce", +// redirect_uri, +// response_type, +// scope, +// state, +// }, +// client_id: "clientId", +// connection: "email", +// email: "foo@example.com", +// send: "link", +// }, +// }); + +// const { otp: otp2 } = getOTP(emails[1]); + +// const authenticateResponse2 = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp: otp2, +// realm: "email", +// username: "foo@example.com", +// }, +// }); + +// const { login_ticket: loginTicket2 } = +// (await authenticateResponse2.json()) as { +// login_ticket: string; +// }; +// const tokenResponse2 = await oauthClient.authorize.$get( +// { +// query: { +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket: loginTicket2, +// nonce: "nonce", +// redirect_uri, +// response_type, +// scope, +// state, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// const accessToken2 = parseJwt( +// new URLSearchParams( +// tokenResponse2.headers.get("location")!.split("#")[1]!, +// ).get("access_token")!, +// ); + +// // this is the id of the primary account +// expect(accessToken2.sub).toBe("auth2|userId"); + +// // ---------------------------- +// // now check silent auth again! +// // ---------------------------- +// const setCookiesHeader2 = tokenResponse2.headers.get("set-cookie")!; +// const { idToken: silentAuthIdTokenPayload2 } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader2, +// oauthClient, +// nonce, +// "clientId", +// ); +// // second time round make sure we get the primary userid again +// expect(silentAuthIdTokenPayload2.sub).toBe("auth2|userId"); +// }); + +// describe("most complex linking flow I can think of", () => { +// it("should follow linked_to chain when logging in with new code user with same email address as existing username-password user THAT IS linked to a code user with a different email address", async () => { +// const token = await getAdminToken(); +// const { managementApp, oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); +// const managementClient = testClient(managementApp, env); + +// // ----------------- +// // create code user - the base user +// // ----------------- + +// await env.data.users.create("tenantId", { +// user_id: "email|the-base-user", +// email: "the-base-user@example.com", +// email_verified: true, +// login_count: 0, +// provider: "email", +// connection: "email", +// is_social: false, +// created_at: new Date().toISOString(), +// updated_at: new Date().toISOString(), +// }); + +// // ----------------- +// // create username-password user with different email address and link to the above user +// // ----------------- + +// await env.data.users.create("tenantId", { +// user_id: "auth2|the-auth2-same-email-user", +// email: "same-email@example.com", +// email_verified: true, +// login_count: 0, +// provider: "auth2", +// connection: "Username-Password-Authentication", +// is_social: false, +// created_at: new Date().toISOString(), +// updated_at: new Date().toISOString(), +// linked_to: "email|the-base-user", +// }); + +// // ----------------- +// // sanity check these users are linked +// // ----------------- + +// const baseUserRes = await managementClient.users[":user_id"].$get( +// { +// param: { +// user_id: "email|the-base-user", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); + +// const baseUser = (await baseUserRes.json()) as UserResponse; + +// expect(baseUser.identities).toEqual([ +// { +// connection: "email", +// provider: "email", +// user_id: "the-base-user", +// isSocial: false, +// }, +// { +// connection: "Username-Password-Authentication", +// provider: "auth2", +// user_id: "the-auth2-same-email-user", +// isSocial: false, +// profileData: { +// email: "same-email@example.com", +// email_verified: true, +// }, +// }, +// ]); + +// // ----------------- +// // Now do a new passwordless flow with a new user with email same-email@example.com +// // ----------------- + +// const passwordlessStartRes = await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "same-email@example.com", +// send: "code", +// }, +// }); +// expect(passwordlessStartRes.status).toBe(200); + +// const { otp } = getOTP(emails[0]); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "same-email@example.com", +// }, +// }); +// expect(authenticateResponse.status).toBe(200); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query: { +// ...AUTH_PARAMS, +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); +// const accessToken = searchParams.get("access_token"); +// const accessTokenPayload = parseJwt(accessToken!); + +// // this proves that we are following the linked user chain +// expect(accessTokenPayload.sub).toBe("email|the-base-user"); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); +// // this proves that we are following the linked user chain +// expect(idTokenPayload.email).toBe("the-base-user@example.com"); + +// // now check silent auth works when logged in with code---------------------------------------- +// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; + +// const { idToken: silentAuthIdTokenPayload } = +// await doSilentAuthRequestAndReturnTokens( +// setCookiesHeader, +// oauthClient, +// AUTH_PARAMS.nonce, +// "clientId", +// ); + +// // this proves the account linking chain is still working +// expect(silentAuthIdTokenPayload.sub).toBe("email|the-base-user"); + +// //------------------------------------------------------------------------------------------------ +// // fetch the base user again now and check we have THREE identities in there +// //------------------------------------------------------------------------------------------------ + +// const baseUserRes2 = await managementClient.users[":user_id"].$get( +// { +// param: { +// user_id: "email|the-base-user", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); + +// const baseUser2 = (await baseUserRes2.json()) as UserResponse; + +// expect(baseUser2.identities).toEqual([ +// { +// connection: "email", +// provider: "email", +// user_id: "the-base-user", +// isSocial: false, +// }, +// { +// connection: "Username-Password-Authentication", +// provider: "auth2", +// user_id: "the-auth2-same-email-user", +// isSocial: false, +// profileData: { +// email: "same-email@example.com", +// email_verified: true, +// }, +// }, +// { +// connection: "email", +// isSocial: false, +// profileData: { +// email: "same-email@example.com", +// email_verified: true, +// }, +// provider: "email", +// user_id: baseUser2.identities[2].user_id, +// }, +// ]); +// }); +// }); + +// it.skip("should only allow a code to be used once", async () => { +// const AUTH_PARAMS = { +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// redirect_uri: "https://login.example.com/callback", +// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, +// scope: "openid profile email", +// state: "state", +// }; + +// const { oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); + +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "foo@example.com", +// send: "code", +// }, +// }); + +// const { otp } = getOTP(emails[0]); + +// const authRes = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "foo@example.com", +// }, +// }); +// expect(authRes.status).toBe(200); + +// // now try to use the same code again +// const authRes2 = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "foo@example.com", +// }, +// }); + +// expect(authRes2.status).toBe(403); +// // this message isn't exactly true! We could check what auth0 does +// expect(await authRes2.json()).toEqual({ +// error: "access_denied", +// error_description: "Wrong email or verification code.", +// }); +// }); + +// it("should not accept an invalid code", async () => { +// const AUTH_PARAMS = { +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// redirect_uri: "https://login.example.com/callback", +// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, +// scope: "openid profile email", +// state: "state", +// }; + +// const { oauthApp, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); + +// await oauthClient.passwordless.start.$post( +// { +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "foo@example.com", +// send: "code", +// }, +// }, +// { +// headers: { +// "content-type": "application/json", +// }, +// }, +// ); + +// const BAD_CODE = "123456"; + +// const authRes = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp: BAD_CODE, +// realm: "email", +// username: "foo@example.com", +// }, +// }); + +// expect(authRes.status).toBe(403); +// }); + +// it("should be case insensitive with email address", async () => { +// const token = await getAdminToken(); +// const { managementApp, oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); +// const managementClient = testClient(managementApp, env); + +// // ------------------------- +// // Create new email user - all lower case email +// // ------------------------- +// const createUserResponse1 = await managementClient.users.$post( +// { +// json: { +// email: "john-doe@example.com", +// connection: "email", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); + +// expect(createUserResponse1.status).toBe(201); +// const newUser1 = (await createUserResponse1.json()) as UserResponse; +// expect(newUser1.email).toBe("john-doe@example.com"); + +// const AUTH_PARAMS = { +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// redirect_uri: "https://login.example.com/callback", +// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, +// scope: "openid profile email", +// state: "state", +// }; + +// // ----------------- +// // Sign in with same user passwordless +// // ----------------- +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// // do we want two tests? one for the username uppercase one for the domain? +// email: "JOHN-DOE@example.com", +// send: "code", +// }, +// }); + +// expect(emails.length).toBe(1); +// const { otp, to } = getOTP(emails[0]); +// expect(to).toBe("john-doe@example.com"); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "JOHN-DOE@example.com", +// }, +// }); + +// if (authenticateResponse.status !== 200) { +// throw new Error(await authenticateResponse.text()); +// } + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// const query = { +// ...AUTH_PARAMS, +// auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// referrer: "https://login.example.com", +// realm: "email", +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get({ +// query, +// }); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); + +// expect(redirectUri.hostname).toBe("login.example.com"); + +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); + +// expect(searchParams.get("state")).toBe("state"); + +// const accessToken = searchParams.get("access_token"); + +// const accessTokenPayload = parseJwt(accessToken!); +// expect(accessTokenPayload.sub).toBe(newUser1.user_id); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); +// expect(idTokenPayload.email).toBe("john-doe@example.com"); +// }); + +// it("should store new user email in lowercase", async () => { +// const { oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); + +// const AUTH_PARAMS = { +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// redirect_uri: "https://login.example.com/callback", +// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, +// scope: "openid profile email", +// state: "state", +// }; + +// // ----------------- +// // New passwordless sign up all uppercase - login2 would stop this... What does auth0.js do? CHECK! +// // ----------------- +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "JOHN-DOE@EXAMPLE.COM", +// send: "code", +// }, +// }); + +// const { otp } = getOTP(emails[0]); +// expect(otp).toBeTypeOf("string"); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// // use lowercase here... TBD +// username: "john-doe@example.com", +// }, +// }); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query: { +// ...AUTH_PARAMS, +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); +// const redirectUri = new URL(tokenResponse.headers.get("location")!); +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); +// const accessToken = searchParams.get("access_token"); + +// const sub = parseJwt(accessToken!).sub; + +// // this means we have created the user +// expect(tokenResponse.status).toBe(302); + +// // Now check in database we are storing in lower case + +// const newLowercaseUser = await env.data.users.get("tenantId", sub); + +// expect(newLowercaseUser!.email).toBe("john-doe@example.com"); +// }); + +// // TO TEST +// // - using expired codes? how can we fast-forward time with wrangler... +// // - more linked accounts +// // more basic error testing e.g. +// // - do not allow code from a different account: we should be fine without this but I can see a way we could mess this up! + +// describe("edge cases", () => { +// it("should login correctly for a code account linked to another account with a different email, when a password account has been registered but not verified", async () => { +// // create a new user with a password +// const token = await getAdminToken(); +// const { managementApp, oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); +// const managementClient = testClient(managementApp, env); + +// // ----------------- +// // user fixtures +// // ----------------- + +// // create new password user +// await env.data.users.create("tenantId", { +// user_id: "auth2|base-user", +// email: "base-user@example.com", +// email_verified: true, +// login_count: 0, +// provider: "auth2", +// connection: "Username-Password-Authentication", +// is_social: false, +// created_at: new Date().toISOString(), +// updated_at: new Date().toISOString(), +// }); +// // create new code user and link this to the password user +// await env.data.users.create("tenantId", { +// user_id: "auth2|code-user", +// email: "code-user@example.com", +// email_verified: true, +// login_count: 0, +// provider: "email", +// connection: "email", +// is_social: false, +// created_at: new Date().toISOString(), +// updated_at: new Date().toISOString(), +// linked_to: "auth2|base-user", +// }); + +// // sanity check - get base user and check identities +// const baseUserRes = await managementClient.users[":user_id"].$get( +// { +// param: { +// user_id: "auth2|base-user", +// }, +// header: { +// "tenant-id": "tenantId", +// }, +// }, +// { +// headers: { +// authorization: `Bearer ${token}`, +// }, +// }, +// ); +// expect(baseUserRes.status).toBe(200); +// const baseUser = (await baseUserRes.json()) as UserResponse; +// expect(baseUser.identities).toEqual([ +// { +// connection: "Username-Password-Authentication", +// isSocial: false, +// provider: "auth2", +// user_id: "base-user", +// }, +// { +// connection: "email", +// isSocial: false, +// profileData: { +// email: "code-user@example.com", +// email_verified: true, +// }, +// provider: "email", +// user_id: "code-user", +// }, +// ]); + +// // ----------------- +// // Now start password sign up with same code-user@example.com email +// // I'm seeing if this affects the code user with the same email address +// // ----------------- +// const createUserResponse = await oauthClient.dbconnections.signup.$post({ +// json: { +// client_id: "clientId", +// connection: "Username-Password-Authentication", +// email: "code-user@example.com", +// password: "Password1234!", +// }, +// }); + +// expect(createUserResponse.status).toBe(200); + +// //----------------- +// // now try and sign in with code-user@example.com code flow +// // I'm testing that the unlinked password user with the same email address does not affect this +// // ----------------- + +// const response = await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "code-user@example.com", +// send: "code", +// }, +// }); + +// expect(response.status).toBe(200); + +// // first email is email validation from sign up above +// const { otp } = getOTP(emails[1]); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "code-user@example.com", +// }, +// }); + +// if (authenticateResponse.status !== 200) { +// throw new Error( +// `Failed to authenticate with status: ${ +// authenticateResponse.status +// } and message: ${await response.text()}`, +// ); +// } + +// expect(authenticateResponse.status).toBe(200); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query: { +// ...AUTH_PARAMS, +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// expect(tokenResponse.status).toBe(302); +// expect(await tokenResponse.text()).toBe("Redirecting"); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); +// const accessToken = searchParams.get("access_token"); +// const accessTokenPayload = parseJwt(accessToken!); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); + +// // these prove that we are getting the code account's primary account! +// expect(accessTokenPayload.sub).toBe("auth2|base-user"); +// expect(idTokenPayload.email).toBe("base-user@example.com"); +// }); + +// it("should ignore un-verified password account when signing up with code account", async () => { +// const { oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); + +// // ----------------- +// // signup new user +// // ----------------- +// const createUserResponse = await oauthClient.dbconnections.signup.$post({ +// json: { +// client_id: "clientId", +// connection: "Username-Password-Authentication", +// email: "same-user-signin@example.com", +// password: "Password1234!", +// }, +// }); + +// expect(createUserResponse.status).toBe(200); + +// const unverifiedPasswordUser = await createUserResponse.json(); + +// //----------------- +// // sign up new code user that has same email address +// //----------------- +// const response = await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "same-user-signin@example.com", +// send: "code", +// }, +// }); + +// if (response.status !== 200) { +// throw new Error(await response.text()); +// } + +// // first email will be email verification +// const { otp } = getOTP(emails[1]); + +// // Authenticate using the code +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "same-user-signin@example.com", +// }, +// }); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// // Trade the ticket for token +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query: { +// ...AUTH_PARAMS, +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// realm: "email", +// }, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// expect(tokenResponse.status).toBe(302); +// expect(await tokenResponse.text()).toBe("Redirecting"); + +// const redirectUri = new URL(tokenResponse.headers.get("location")!); + +// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); + +// const accessToken = searchParams.get("access_token"); + +// const accessTokenPayload = parseJwt(accessToken!); +// expect(accessTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); + +// const idToken = searchParams.get("id_token"); +// const idTokenPayload = parseJwt(idToken!); +// expect(idTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); +// expect(idTokenPayload.email_verified).toBe(true); +// }); + +// // tickets are used by a few flows so this probably should not be here +// it("should only allow a ticket to be used once", async () => { +// const AUTH_PARAMS = { +// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", +// redirect_uri: "https://login.example.com/callback", +// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, +// scope: "openid profile email", +// state: "state", +// }; +// const { oauthApp, emails, env } = await getTestServer(); +// const oauthClient = testClient(oauthApp, env); + +// await oauthClient.passwordless.start.$post({ +// json: { +// authParams: AUTH_PARAMS, +// client_id: "clientId", +// connection: "email", +// email: "foo@example.com", +// send: "code", +// }, +// }); +// const { otp } = getOTP(emails[0]); + +// const authenticateResponse = await oauthClient.co.authenticate.$post({ +// json: { +// client_id: "clientId", +// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", +// otp, +// realm: "email", +// username: "foo@example.com", +// }, +// }); +// expect(authenticateResponse.status).toBe(200); + +// const { login_ticket } = (await authenticateResponse.json()) as { +// login_ticket: string; +// }; + +// // ----------------- +// // Trade the ticket for token once so it is used +// // ----------------- + +// const query = { +// ...AUTH_PARAMS, +// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", +// client_id: "clientId", +// login_ticket, +// realm: "email", +// }; + +// const tokenResponse = await oauthClient.authorize.$get( +// { +// query, +// }, +// { +// headers: { +// referrer: "https://login.example.com", +// }, +// }, +// ); + +// expect(tokenResponse.status).toBe(302); +// expect(await tokenResponse.text()).toBe("Redirecting"); + +// // ----------------- +// // Now try trading ticket again and it should not work +// // ----------------- +// const rejectedSecondTicketUsageRes = await oauthClient.authorize.$get({ +// query, +// }); + +// expect(rejectedSecondTicketUsageRes.status).toBe(403); +// expect(await rejectedSecondTicketUsageRes.text()).toBe( +// "Ticket not found", +// ); +// }); +// }); +// }); diff --git a/test/integration/flows/social.spec.ts b/test/integration/flows/social.spec.ts index ea4a5ca7e..c2c427aeb 100644 --- a/test/integration/flows/social.spec.ts +++ b/test/integration/flows/social.spec.ts @@ -695,19 +695,22 @@ describe("social sign on", () => { const oauthClient = testClient(oauthApp, env); // ----------------- - // signup new user + // create new user // ----------------- - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - // matches social sign up we will do next - email: "örjan.lindström@example.com", - password: "Password1234!", - }, + await env.data.users.create("tenantId", { + email: "örjan.lindström@example.com", + email_verified: false, + name: "örjan", + provider: "email", + connection: "Username-Password-Authentication", + user_id: "email|123456789012345678901", + last_ip: "", + login_count: 0, + is_social: false, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), }); - expect(createUserResponse.status).toBe(200); //----------------- // sign up new social user that has same email address diff --git a/yarn.lock b/yarn.lock index acd23554f..9fa4dd93c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,17 +19,17 @@ dependencies: openapi3-ts "^4.1.2" -"@authhero/adapter-interfaces@^0.32.1": - version "0.32.1" - resolved "https://registry.yarnpkg.com/@authhero/adapter-interfaces/-/adapter-interfaces-0.32.1.tgz#68589c17eb2896dbe93323809437b8dfbee0b432" - integrity sha512-3ovoDMTyD47kMe2xHRirQ7Y02TeLHymx3u76x7rXodV1up7t1ERWmQnL0FmyOraKy5SrlM5msLPh2CYgBikBAw== +"@authhero/adapter-interfaces@^0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@authhero/adapter-interfaces/-/adapter-interfaces-0.33.0.tgz#1efa432a6b4f4a733a853027346746a0d3e979f5" + integrity sha512-jQpl8G3A1n+oxUWdghZOkP3lGJOgXa5CpdT8TNXa5KjW0KxChgn/nwhHSeqd6P3IdmnYkhecxYZRuPa5u0yecw== -"@authhero/kysely-adapter@0.25.2": - version "0.25.2" - resolved "https://registry.yarnpkg.com/@authhero/kysely-adapter/-/kysely-adapter-0.25.2.tgz#b3aa3996e65808d49aecfab8125602bb9ee81d31" - integrity sha512-T0ahDmy0J5o1c6Ol0kxQbQ4tccuWZtIDbWG+XuLwCmdXJsEDTeNlMUvNQBi+5x5L6D4PVBNHLVX9ktzKugBfQQ== +"@authhero/kysely-adapter@0.25.3": + version "0.25.3" + resolved "https://registry.yarnpkg.com/@authhero/kysely-adapter/-/kysely-adapter-0.25.3.tgz#34015033200462bd17a054ed709fd38e87849404" + integrity sha512-y91Q/QR0dd6HGC5SY4UvqXDiJVjEmO0FeSToOw5nFNbXazN6Jyq/RwVSWRNMp8mH5/eIONdUpB0xnmbPamHD6w== dependencies: - "@authhero/adapter-interfaces" "^0.32.1" + "@authhero/adapter-interfaces" "^0.33.0" kysely "^0.27.4" nanoid "^5.0.8" @@ -108,18 +108,10 @@ resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz#a2a15eef4d154076527ccb9049d216b4c80717fe" integrity sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg== -"@cloudflare/workers-shared@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@cloudflare/workers-shared/-/workers-shared-0.10.0.tgz#8cdb7dcb8de1babd8ca431108e2b17ccb83a6a21" - integrity sha512-j3EwZBc9ctavmFVOQT1gqztRO/Plx4ZR0LMEEOif+5YoCcuD1P7/NEjlODPMc5a1w+8+7A/H+Ci8Ihd55+x0Zw== - dependencies: - mime "^3.0.0" - zod "^3.22.3" - -"@cloudflare/workers-types@4.20241205.0": - version "4.20241205.0" - resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20241205.0.tgz#210d532063d9a11e91ee9c8615f9a666674e08d6" - integrity sha512-pj1VKRHT/ScQbHOIMFODZaNAlJHQHdBSZXNIdr9ebJzwBff9Qz8VdqhbhggV7f+aUEh8WSbrsPIo4a+WtgjUvw== +"@cloudflare/workers-types@4.20241216.0": + version "4.20241216.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-4.20241216.0.tgz#fe8aa1fa5b5c6d6bd76bde43ef10ee53b5845d85" + integrity sha512-PGIINXS+aE9vD2GYyWXfRG+VyxxceRkGDCoPxqwUweh1Bfv75HVotyL/adJ7mRVwh3XZDifGBdTaLReTT+Fcog== "@colors/colors@1.5.0": version "1.5.0" @@ -433,10 +425,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.15.0": - version "9.15.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.15.0.tgz#df0e24fe869143b59731942128c19938fdbadfb5" - integrity sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg== +"@eslint/js@9.17.0": + version "9.17.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.17.0.tgz#1523e586791f80376a6f8398a3964455ecc651ec" + integrity sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w== "@eslint/object-schema@^2.1.5": version "2.1.5" @@ -455,18 +447,18 @@ resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== -"@hono/zod-openapi@0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@hono/zod-openapi/-/zod-openapi-0.16.2.tgz#4306ed18a929d4c876a8aca2ecfa55a95a118d8b" - integrity sha512-MxlZzvHTXmOihKgIqYrEH5TysIs3Ep0PlTfmdaXe22HR0o2BULbpWW3F7W4I+2jAbt9rxLbxcs3aE2MXnIuAbw== +"@hono/zod-openapi@0.18.3": + version "0.18.3" + resolved "https://registry.yarnpkg.com/@hono/zod-openapi/-/zod-openapi-0.18.3.tgz#2ae5e693df4a5db40fefb68e6221abe15bbe1f45" + integrity sha512-bNlRDODnp7P9Fs13ZPajEOt13G0XwXKfKRHMEFCphQsFiD1Y+twzHaglpNAhNcflzR1DQwHY92ZS06b4LTPbIQ== dependencies: "@asteasolutions/zod-to-openapi" "^7.1.0" - "@hono/zod-validator" "0.3.0" + "@hono/zod-validator" "^0.4.1" -"@hono/zod-validator@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@hono/zod-validator/-/zod-validator-0.3.0.tgz#987ffccdb5fc1b5fa87c321ad161e5f1f159e6a6" - integrity sha512-7XcTk3yYyk6ldrO/VuqsroE7stvDZxHJQcpATRAyha8rUxJNBPV3+6waDrARfgEqxOVlzIadm3/6sE/dPseXgQ== +"@hono/zod-validator@^0.4.1": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@hono/zod-validator/-/zod-validator-0.4.2.tgz#cac46e0d5b2440b794fef58747d3dbb47ae7f92d" + integrity sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g== "@humanfs/core@^0.19.1": version "0.19.1" @@ -1705,12 +1697,12 @@ dependencies: undici-types "~6.20.0" -"@types/node@22.9.1": - version "22.9.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.1.tgz#bdf91c36e0e7ecfb7257b2d75bf1b206b308ca71" - integrity sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg== +"@types/node@22.10.2": + version "22.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.2.tgz#a485426e6d1fdafc7b0d4c7b24e2c78182ddabb9" + integrity sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ== dependencies: - undici-types "~6.19.8" + undici-types "~6.20.0" "@types/node@~20.12.8": version "20.12.14" @@ -1785,62 +1777,62 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz#2ee073c421f4e81e02d10e731241664b6253b23c" - integrity sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w== +"@typescript-eslint/eslint-plugin@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz#992e5ac1553ce20d0d46aa6eccd79dc36dedc805" + integrity sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.17.0" - "@typescript-eslint/type-utils" "8.17.0" - "@typescript-eslint/utils" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/type-utils" "8.18.1" + "@typescript-eslint/utils" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.17.0.tgz#2ee972bb12fa69ac625b85813dc8d9a5a053ff52" - integrity sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg== +"@typescript-eslint/parser@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.1.tgz#c258bae062778b7696793bc492249027a39dfb95" + integrity sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA== dependencies: - "@typescript-eslint/scope-manager" "8.17.0" - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/typescript-estree" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz#a3f49bf3d4d27ff8d6b2ea099ba465ef4dbcaa3a" - integrity sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg== +"@typescript-eslint/scope-manager@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz#52cedc3a8178d7464a70beffed3203678648e55b" + integrity sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ== dependencies: - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" -"@typescript-eslint/type-utils@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz#d326569f498cdd0edf58d5bb6030b4ad914e63d3" - integrity sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw== +"@typescript-eslint/type-utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz#10f41285475c0bdee452b79ff7223f0e43a7781e" + integrity sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ== dependencies: - "@typescript-eslint/typescript-estree" "8.17.0" - "@typescript-eslint/utils" "8.17.0" + "@typescript-eslint/typescript-estree" "8.18.1" + "@typescript-eslint/utils" "8.18.1" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.17.0.tgz#ef84c709ef8324e766878834970bea9a7e3b72cf" - integrity sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA== +"@typescript-eslint/types@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.1.tgz#d7f4f94d0bba9ebd088de840266fcd45408a8fff" + integrity sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw== -"@typescript-eslint/typescript-estree@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz#40b5903bc929b1e8dd9c77db3cb52cfb199a2a34" - integrity sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw== +"@typescript-eslint/typescript-estree@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz#2a86cd64b211a742f78dfa7e6f4860413475367e" + integrity sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg== dependencies: - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/visitor-keys" "8.17.0" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/visitor-keys" "8.18.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -1848,22 +1840,22 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.17.0.tgz#41c05105a2b6ab7592f513d2eeb2c2c0236d8908" - integrity sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w== +"@typescript-eslint/utils@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.1.tgz#c4199ea23fc823c736e2c96fd07b1f7235fa92d5" + integrity sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.17.0" - "@typescript-eslint/types" "8.17.0" - "@typescript-eslint/typescript-estree" "8.17.0" + "@typescript-eslint/scope-manager" "8.18.1" + "@typescript-eslint/types" "8.18.1" + "@typescript-eslint/typescript-estree" "8.18.1" -"@typescript-eslint/visitor-keys@8.17.0": - version "8.17.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz#4dbcd0e28b9bf951f4293805bf34f98df45e1aa8" - integrity sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg== +"@typescript-eslint/visitor-keys@8.18.1": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz#344b4f6bc83f104f514676facf3129260df7610a" + integrity sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ== dependencies: - "@typescript-eslint/types" "8.17.0" + "@typescript-eslint/types" "8.18.1" eslint-visitor-keys "^4.2.0" "@vitest/expect@2.1.8": @@ -2073,10 +2065,10 @@ archy@~1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== -arctic@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/arctic/-/arctic-2.3.0.tgz#59c95739d52963c36a54367442236103db20ab62" - integrity sha512-ImueY1iKm044nMVxQGsLvzSFLrLsqCIpsvohZprK2l8o3ypXjoSKiMBlxBBdoFpAG0iC78cJ6J/vyLpvdLQlkw== +arctic@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/arctic/-/arctic-2.3.2.tgz#977177ded4e94061b65816d6703cb006b5ead9b1" + integrity sha512-soUbxjMjmRRWTyFn9eoJ3PMCjjlLC6ivZ8NZ+ci2erpmkRv+YBPhY1bKTbmIBnDy29sc0LQNXaMxAtQwjQNazg== dependencies: "@oslojs/crypto" "1.0.1" "@oslojs/encoding" "1.1.0" @@ -2208,16 +2200,17 @@ assertion-error@^2.0.1: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== -authhero@^0.25.2: - version "0.25.2" - resolved "https://registry.yarnpkg.com/authhero/-/authhero-0.25.2.tgz#afdff974467759abd29817f2b7908864a97ff41e" - integrity sha512-2pGmpxXePIkb8WBiGme6f8NFr/jvbIxdpk8gcFuQdw4PGMC7WzRcvNlsB52uqKwQmmDF5g90j8YmkoklGo27Vg== +authhero@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/authhero/-/authhero-0.26.0.tgz#0188b271ba5f1543bc38dbaf5dd416727a244651" + integrity sha512-zHdHW5KQjlvkZzr0DYL2KE5/wrZwXuU3LxMNvc1fEc13F2Q1Q+fVABWUJ1djUWhBiX21RDqW4VZ2AyAD7rePug== dependencies: - "@authhero/adapter-interfaces" "^0.32.1" + "@authhero/adapter-interfaces" "^0.33.0" "@peculiar/x509" "^1.12.3" bcrypt "^5.1.1" bcryptjs "^2.4.3" oslo "^1.2.1" + zxcvbn "^4.4.2" autoprefixer@^10.4.20: version "10.4.20" @@ -2266,10 +2259,10 @@ before-after-hook@^3.0.2: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d" integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A== -better-sqlite3@11.5.0: - version "11.5.0" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.5.0.tgz#58faa51e02845a578dd154f0083487132ead0695" - integrity sha512-e/6eggfOutzoK0JWiU36jsisdWoHOfN9iWiW/SieKvb7SAa6aGNmBM/UKyp+/wWSXpLlWNN8tCPwoDNPhzUvuQ== +better-sqlite3@11.7.0: + version "11.7.0" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.7.0.tgz#3eaa0f54f9e57d0a100d980e42320f8b9a4cd676" + integrity sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig== dependencies: bindings "^1.5.0" prebuild-install "^7.1.1" @@ -2454,7 +2447,7 @@ chalk@^2.3.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.2: +chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2792,7 +2785,7 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" -cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.5: +cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -3063,10 +3056,10 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" -dotenv@16.4.5: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +dotenv@16.4.7: + version "16.4.7" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" + integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== duplexer2@~0.1.0: version "0.1.4" @@ -3432,17 +3425,17 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@9.15.0: - version "9.15.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.15.0.tgz#77c684a4e980e82135ebff8ee8f0a9106ce6b8a6" - integrity sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw== +eslint@9.17.0: + version "9.17.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.17.0.tgz#faa1facb5dd042172fdc520106984b5c2421bb0c" + integrity sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" "@eslint/config-array" "^0.19.0" "@eslint/core" "^0.9.0" "@eslint/eslintrc" "^3.2.0" - "@eslint/js" "9.15.0" + "@eslint/js" "9.17.0" "@eslint/plugin-kit" "^0.2.3" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" @@ -3451,7 +3444,7 @@ eslint@9.15.0: "@types/json-schema" "^7.0.15" ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.5" + cross-spawn "^7.0.6" debug "^4.3.2" escape-string-regexp "^4.0.0" eslint-scope "^8.2.0" @@ -3622,10 +3615,10 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-xml-parser@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37" - integrity sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg== +fast-xml-parser@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz#a7e665ff79b7919100a5202f23984b6150f9b31e" + integrity sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w== dependencies: strnum "^1.0.5" @@ -4097,10 +4090,10 @@ hono-openapi-middlewares@^1.0.11: resolved "https://registry.yarnpkg.com/hono-openapi-middlewares/-/hono-openapi-middlewares-1.0.11.tgz#9264fae988b3b073b17fd5a0ef8b6f61f7b5f291" integrity sha512-1yHKr9LHsl7QDDCS4u2REyGGgtUyO6J/L78ncINfFXSuAXCglgT35ctyLylP3iI5XhgE9wuwh6eIDVeW1DWAVA== -hono@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.4.0.tgz#df456c61fab856db3d546869ec8d86839ff31b54" - integrity sha512-Bb2GHk8jmlLIuxc3U+7UBGOoA5lByJTAFnRdH2N2fqEVy9TEQzJ9saIJUQ/ZqBvEvgEFe7UjPFNSFi8cyeU+3Q== +hono@4.6.14: + version "4.6.14" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.6.14.tgz#f83f51e81b8ae5611dab459570990bf4c977d20c" + integrity sha512-j4VkyUp2xazGJ8eCCLN1Vm/bxdvm/j5ZuU9AIjLu9vapn2M44p9L3Ktr9Vnb2RN2QtcR/wVjZVMlT5k7GJQgPw== hook-std@^3.0.0: version "3.0.0" @@ -4213,10 +4206,10 @@ husky@9.1.7: resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d" integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA== -i18next@23.16.8: - version "23.16.8" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.16.8.tgz#3ae1373d344c2393f465556f394aba5a9233b93a" - integrity sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg== +i18next@24.1.2: + version "24.1.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.1.2.tgz#6dd0bcf6d50d8d61227b0a3aefb653885d267d09" + integrity sha512-th/075GW0Ub1gYDMHLiZXMGSfGv1aP1VqjT3fma/12hNHCNlH8oJMftvlDzycT/R+KoULWk+xLU8H1JRwV85qw== dependencies: "@babel/runtime" "^7.23.2" @@ -4854,10 +4847,10 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" -knip@5.37.1: - version "5.37.1" - resolved "https://registry.yarnpkg.com/knip/-/knip-5.37.1.tgz#3c3e91c425dfb35be68b4d12cc0b9ee3cde794e8" - integrity sha512-69gjKj5lLsLXcIPXlHyFfX5AOHgRdh/iXH8gUqvmsHtjqoWhOATeXZDjvvemmgw7KxbWbUzxBNbpjhtJWzgqGA== +knip@5.41.0: + version "5.41.0" + resolved "https://registry.yarnpkg.com/knip/-/knip-5.41.0.tgz#c186d27df55d10fe38ce69ed66fcb0c7a44bae1a" + integrity sha512-W8omBs+jhC/P/A3kC0xdEGrhYVmsmWafUVz0wzQjYfoaK0YNEBPPLptUeqwQl6ihYVqzb/X0zs50gY9Akw1Bww== dependencies: "@nodelib/fs.walk" "1.2.8" "@snyk/github-codeowners" "1.1.0" @@ -4870,7 +4863,7 @@ knip@5.37.1: picocolors "^1.1.0" picomatch "^4.0.1" pretty-ms "^9.0.0" - smol-toml "^1.3.0" + smol-toml "^1.3.1" strip-json-comments "5.0.1" summary "2.1.0" zod "^3.22.4" @@ -5019,12 +5012,7 @@ libnpmversion@^7.0.0: proc-log "^5.0.0" semver "^7.3.7" -lilconfig@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== - -lilconfig@^3.0.0, lilconfig@^3.1.1: +lilconfig@^3.0.0, lilconfig@^3.1.1, lilconfig@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== @@ -5725,10 +5713,10 @@ ms@^2.1.2, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msw@^2.6.5: - version "2.6.6" - resolved "https://registry.yarnpkg.com/msw/-/msw-2.6.6.tgz#4b9babab8d4414a6859f4a65ce4eacb8e5f173f0" - integrity sha512-npfIIVRHKQX3Lw4aLWX4wBh+lQwpqdZNyJYB5K/+ktK8NhtkdsTxGK7WDrgknozcVyRI7TOqY6yBS9j2FTR+YQ== +msw@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.0.tgz#d13ff87f7e018fc4c359800ff72ba5017033fb56" + integrity sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw== dependencies: "@bundled-es-modules/cookie" "^2.0.1" "@bundled-es-modules/statuses" "^1.0.1" @@ -5739,12 +5727,12 @@ msw@^2.6.5: "@open-draft/until" "^2.1.0" "@types/cookie" "^0.6.0" "@types/statuses" "^2.0.4" - chalk "^4.1.2" graphql "^16.8.1" headers-polyfill "^4.0.2" is-node-process "^1.2.0" outvariant "^1.4.3" path-to-regexp "^6.3.0" + picocolors "^1.1.1" strict-event-emitter "^0.5.1" type-fest "^4.26.1" yargs "^17.7.2" @@ -6544,17 +6532,17 @@ pkg-conf@^2.1.0: find-up "^2.0.0" load-json-file "^4.0.0" -playwright-core@1.44.1: - version "1.44.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.44.1.tgz#53ec975503b763af6fc1a7aa995f34bc09ff447c" - integrity sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA== +playwright-core@1.49.1: + version "1.49.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015" + integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg== -playwright@1.44.1: - version "1.44.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.44.1.tgz#5634369d777111c1eea9180430b7a184028e7892" - integrity sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg== +playwright@1.49.1: + version "1.49.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c" + integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA== dependencies: - playwright-core "1.44.1" + playwright-core "1.49.1" optionalDependencies: fsevents "2.3.2" @@ -6688,10 +6676,10 @@ prettier-plugin-tailwindcss@0.6.9: resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz#db84c32918eae9b44e5a5f0aa4d1249cc39fa739" integrity sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg== -prettier@3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== +prettier@3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" @@ -7322,7 +7310,7 @@ smart-buffer@^4.2.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -smol-toml@^1.3.0: +smol-toml@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.1.tgz#d9084a9e212142e3cab27ef4e2b8e8ba620bfe15" integrity sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ== @@ -7693,10 +7681,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tailwindcss@3.4.15: - version "3.4.15" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.15.tgz#04808bf4bf1424b105047d19e7d4bfab368044a9" - integrity sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw== +tailwindcss@3.4.16: + version "3.4.16" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.16.tgz#35a7c3030844d6000fc271878db4096b6a8d2ec9" + integrity sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" @@ -7707,7 +7695,7 @@ tailwindcss@3.4.15: glob-parent "^6.0.2" is-glob "^4.0.3" jiti "^1.21.6" - lilconfig "^2.1.0" + lilconfig "^3.1.3" micromatch "^4.0.8" normalize-path "^3.0.0" object-hash "^3.0.0" @@ -8002,14 +7990,14 @@ typed-array-length@^1.0.6: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript-eslint@^8.15.0: - version "8.17.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.17.0.tgz#fa4033c26b3b40f778287bc12918d985481b220b" - integrity sha512-409VXvFd/f1br1DCbuKNFqQpXICoTB+V51afcwG1pn1a3Cp92MqAUges3YjwEdQ0cMUoCIodjVDAYzyD8h3SYA== +typescript-eslint@^8.18.1: + version "8.18.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.18.1.tgz#197b284b6769678ed77d9868df180eeaf61108eb" + integrity sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ== dependencies: - "@typescript-eslint/eslint-plugin" "8.17.0" - "@typescript-eslint/parser" "8.17.0" - "@typescript-eslint/utils" "8.17.0" + "@typescript-eslint/eslint-plugin" "8.18.1" + "@typescript-eslint/parser" "8.18.1" + "@typescript-eslint/utils" "8.18.1" typescript@5.7.2: version "5.7.2" @@ -8041,11 +8029,6 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.19.8: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - undici-types@~6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" @@ -8371,13 +8354,12 @@ workerd@1.20241205.0: "@cloudflare/workerd-linux-arm64" "1.20241205.0" "@cloudflare/workerd-windows-64" "1.20241205.0" -wrangler@3.93.0: - version "3.93.0" - resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.93.0.tgz#9b34ba70b0cc9708a4a161236bf7a72fd7a62b8b" - integrity sha512-+wfxjOrtm6YgDS+NdJkB6aiBIS3ED97mNRQmfrEShRJW4pVo4sWY6oQ1FsGT+j4tGHplrTbWCE6U5yTgjNW/lw== +wrangler@3.96.0: + version "3.96.0" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.96.0.tgz#d62b11bd2968f44a1705706dc3ceac27137915c6" + integrity sha512-KjbHTUnwTa5eKl3hzv2h6nHBfAsbUkdurL7f6Y288/Bdn6tcEis13jLVR/nw/eWa3tNCBG1xOMZJboUyzWcC1g== dependencies: "@cloudflare/kv-asset-handler" "0.3.4" - "@cloudflare/workers-shared" "0.10.0" "@esbuild-plugins/node-globals-polyfill" "^0.2.3" "@esbuild-plugins/node-modules-polyfill" "^0.2.2" blake3-wasm "^2.1.5" @@ -8546,7 +8528,17 @@ zod-validation-error@^3.0.3: resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-3.4.0.tgz#3a8a1f55c65579822d7faa190b51336c61bee2a6" integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ== -zod@3.23.8, zod@^3.22.3, zod@^3.22.4: +zod@3.24.1: + version "3.24.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee" + integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A== + +zod@^3.22.3, zod@^3.22.4: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== + +zxcvbn@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30" + integrity sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ== From ef2e3b890da315397bb2186439fd321b3520e37a Mon Sep 17 00:00:00 2001 From: Markus Ahlstrand Date: Tue, 17 Dec 2024 09:53:32 +0100 Subject: [PATCH 2/4] fix: moved dbconnections to authhero --- src/utils/getCountAsInt.ts | 12 - test/integration/flows/code-flow.spec.ts | 1533 ----------------- .../integration/flows/magic-link-flow.spec.ts | 724 -------- test/integration/flows/password.spec.ts | 1098 ------------ test/integration/flows/social.spec.ts | 5 +- 5 files changed, 2 insertions(+), 3370 deletions(-) delete mode 100644 src/utils/getCountAsInt.ts delete mode 100644 test/integration/flows/code-flow.spec.ts delete mode 100644 test/integration/flows/magic-link-flow.spec.ts delete mode 100644 test/integration/flows/password.spec.ts diff --git a/src/utils/getCountAsInt.ts b/src/utils/getCountAsInt.ts deleted file mode 100644 index fd5ea9092..000000000 --- a/src/utils/getCountAsInt.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default function getCountAsInt(count: string | number | bigint) { - // VScode complains that parseInt only accepts a string BUT the project builds & lints - if (typeof count === "string") { - return parseInt(count, 10); - } - - if (typeof count === "bigint") { - return Number(count); - } - - return count; -} diff --git a/test/integration/flows/code-flow.spec.ts b/test/integration/flows/code-flow.spec.ts deleted file mode 100644 index 9b5f11a46..000000000 --- a/test/integration/flows/code-flow.spec.ts +++ /dev/null @@ -1,1533 +0,0 @@ -// import { describe, it, expect } from "vitest"; -// import { parseJwt } from "../../../src/utils/parse-jwt"; -// import { UserResponse } from "../../../src/types/auth0"; -// import { doSilentAuthRequestAndReturnTokens } from "../helpers/silent-auth"; -// import { testClient } from "hono/testing"; -// import { getAdminToken } from "../helpers/token"; -// import { getTestServer } from "../helpers/test-server"; -// import { EmailOptions } from "../../../src/services/email/EmailOptions"; -// import { snapshotEmail } from "../helpers/playwrightSnapshots"; -// import { AuthorizationResponseType, Log } from "authhero"; - -// const AUTH_PARAMS = { -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// redirect_uri: "https://login.example.com/callback", -// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, -// scope: "openid profile email", -// state: "state", -// }; - -// function getOTP(email: EmailOptions) { -// const codeEmailBody = email.content[0].value; -// // this ignores number prefixed by hashes so we don't match CSS colours -// const otps = codeEmailBody.match(/(?!#).[0-9]{6}/g)!; -// const otp = otps[0].slice(1); - -// const to = email.to[0].email; - -// return { otp, to }; -// } - -// describe("code-flow", () => { -// it("should create new user when email does not exist", async () => { -// const token = await getAdminToken(); -// const { managementApp, oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); -// const managementClient = testClient(managementApp, env); - -// // ----------------- -// // Doing a new signup here, so expect this email not to exist -// // ----------------- -// const resInitialQuery = await managementClient["users-by-email"].$get( -// { -// query: { -// email: "test@example.com", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); -// const results = await resInitialQuery.json(); -// expect(results).toEqual([]); - -// // ----------------- -// // Start the passwordless flow -// // ----------------- -// const response = await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "test@example.com", -// // can be code or link -// send: "code", -// }, -// }); - -// if (response.status !== 200) { -// throw new Error(await response.text()); -// } - -// const { otp } = getOTP(emails[0]); - -// await snapshotEmail(emails[0], true); - -// const { -// logs: [clsLog], -// } = await env.data.logs.list("tenantId", { -// page: 0, -// per_page: 100, -// include_totals: true, -// }); -// expect(clsLog).toMatchObject({ -// type: "cls", -// tenant_id: "tenantId", -// user_id: "", // this is correct. Auth0 does not tie this log to a user account -// description: "test@example.com", // we only know which user it is by looking at the description field -// }); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "test@example.com", -// }, -// }); - -// if (authenticateResponse.status !== 200) { -// throw new Error( -// `Failed to authenticate with status: ${ -// authenticateResponse.status -// } and message: ${await response.text()}`, -// ); -// } - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// const query = { -// ...AUTH_PARAMS, -// auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// referrer: "https://login.example.com", -// realm: "email", -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get({ -// query, -// }); - -// expect(tokenResponse.status).toBe(302); -// expect(await tokenResponse.text()).toBe("Redirecting"); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); - -// expect(redirectUri.hostname).toBe("login.example.com"); - -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - -// expect(searchParams.get("state")).toBe("state"); - -// const accessToken = searchParams.get("access_token"); - -// const accessTokenPayload = parseJwt(accessToken!); -// expect(accessTokenPayload.aud).toBe("default"); -// expect(accessTokenPayload.iss).toBe("https://example.com/"); -// expect(accessTokenPayload.scope).toBe("openid profile email"); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); -// expect(idTokenPayload.email).toBe("test@example.com"); -// expect(idTokenPayload.aud).toBe("clientId"); - -// const { logs } = await env.data.logs.list("tenantId", { -// page: 0, -// per_page: 100, -// include_totals: true, -// }); - -// expect(logs.length).toBe(2); -// const log = logs.find((log: Log) => log.type === "scoa"); - -// expect(log).toMatchObject({ -// type: "scoa", -// tenant_id: "tenantId", -// user_id: accessTokenPayload.sub, -// user_name: "test@example.com", -// }); - -// // now check silent auth works when logged in with code---------------------------------------- -// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - -// const { idToken: silentAuthIdTokenPayload } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader, -// oauthClient, -// AUTH_PARAMS.nonce, -// "clientId", -// ); - -// expect(silentAuthIdTokenPayload.sub).toContain("email|"); -// expect(silentAuthIdTokenPayload).toMatchObject({ -// aud: "clientId", -// name: "test@example.com", -// email: "test@example.com", -// email_verified: true, -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// iss: "https://example.com/", -// }); - -// // ---------------------------- -// // Now log in (previous flow was signup) -// // ---------------------------- -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "test@example.com", -// send: "code", -// }, -// }); - -// const { otp: otpLogin } = getOTP(emails[1]); - -// const authRes2 = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp: otpLogin, -// realm: "email", -// username: "test@example.com", -// }, -// }); - -// const { login_ticket: loginTicket2 } = (await authRes2.json()) as { -// login_ticket: string; -// }; - -// const tokenRes2 = await oauthClient.authorize.$get( -// { -// query: { -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket: loginTicket2, -// ...AUTH_PARAMS, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// // ---------------------------- -// // Now silent auth again - confirms that logging in works -// // ---------------------------- -// const setCookiesHeader2 = tokenRes2.headers.get("set-cookie")!; -// const { idToken: silentAuthIdTokenPayload2 } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader2, -// oauthClient, -// AUTH_PARAMS.nonce, -// "clientId", -// ); - -// expect(silentAuthIdTokenPayload2.sub).toEqual(silentAuthIdTokenPayload.sub); -// expect(silentAuthIdTokenPayload2).toMatchObject({ -// aud: "clientId", -// name: "test@example.com", -// email: "test@example.com", -// email_verified: true, -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// iss: "https://example.com/", -// }); -// }); -// it("is an existing primary user", async () => { -// const token = await getAdminToken(); -// const { managementApp, oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); -// const managementClient = testClient(managementApp, env); - -// // ----------------- -// // Create the user to log in with the code -// // ----------------- -// env.data.users.create("tenantId", { -// user_id: "email|userId2", -// email: "bar@example.com", -// email_verified: true, -// name: "", -// nickname: "", -// picture: "https://example.com/foo.png", -// login_count: 0, -// provider: "email", -// connection: "email", -// is_social: false, -// created_at: new Date().toISOString(), -// updated_at: new Date().toISOString(), -// }); - -// const resInitialQuery = await managementClient["users-by-email"].$get( -// { -// query: { -// email: "bar@example.com", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); -// expect(resInitialQuery.status).toBe(200); - -// // ----------------- -// // Start the passwordless flow -// // ----------------- -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "bar@example.com", -// send: "code", -// }, -// }); - -// const { otp } = getOTP(emails[0]); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "bar@example.com", -// }, -// }); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// const query = { -// ...AUTH_PARAMS, -// auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// referrer: "https://login.example.com", -// realm: "email", -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get({ -// query, -// }); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); - -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - -// const accessToken = searchParams.get("access_token"); - -// const accessTokenPayload = parseJwt(accessToken!); -// expect(accessTokenPayload.sub).toBe("email|userId2"); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); -// expect(idTokenPayload.email).toBe("bar@example.com"); - -// // now check silent auth works when logged in with code---------------------------------------- -// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - -// const { idToken: silentAuthIdTokenPayload } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader, -// oauthClient, -// AUTH_PARAMS.nonce, -// "clientId", -// ); - -// expect(silentAuthIdTokenPayload.sub).toBe("email|userId2"); -// }); -// it("is an existing linked user", async () => { -// const { oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); - -// // ----------------- -// // Create the linked user to log in with the magic link -// // ----------------- -// env.data.users.create("tenantId", { -// user_id: "email|userId2", -// // same email address as existing primary user... but this isn't needed -// // do we need more tests where this is different? In case I've taken shortcuts looking up by email address... -// email: "foo@example.com", -// email_verified: true, -// name: "", -// nickname: "", -// picture: "https://example.com/foo.png", -// login_count: 0, -// provider: "email", -// connection: "email", -// is_social: false, -// created_at: new Date().toISOString(), -// updated_at: new Date().toISOString(), -// linked_to: "auth2|userId", -// }); - -// // ----------------- -// // Start the passwordless flow -// // ----------------- -// await oauthClient.passwordless.start.$post( -// { -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "foo@example.com", -// send: "code", -// }, -// }, -// { -// headers: { -// "content-type": "application/json", -// }, -// }, -// ); - -// const { otp } = getOTP(emails[0]); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "foo@example.com", -// }, -// }); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query: { -// ...AUTH_PARAMS, -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); - -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - -// const accessToken = searchParams.get("access_token"); - -// const accessTokenPayload = parseJwt(accessToken!); -// // this shows we are getting the primary user -// expect(accessTokenPayload.sub).toBe("auth2|userId"); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); -// expect(idTokenPayload.email).toBe("foo@example.com"); - -// // now check silent auth works when logged in with code---------------------------------------- -// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - -// const { idToken: silentAuthIdTokenPayload } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader, -// oauthClient, -// AUTH_PARAMS.nonce, -// "clientId", -// ); - -// // getting the primary user back again -// expect(silentAuthIdTokenPayload.sub).toBe("auth2|userId"); -// }); - -// it("should return existing username-primary account when logging in with new code sign on with same email address", async () => { -// const token = await getAdminToken(); -// const { managementApp, oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); -// const managementClient = testClient(managementApp, env); - -// const nonce = "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM"; -// const redirect_uri = "https://login.example.com/callback"; -// const response_type = AuthorizationResponseType.TOKEN_ID_TOKEN; -// const scope = "openid profile email"; -// const state = "state"; - -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: { -// nonce, -// redirect_uri, -// response_type, -// scope, -// state, -// }, -// client_id: "clientId", -// connection: "email", -// // this email already exists as a Username-Password-Authentication user -// email: "foo@example.com", -// send: "link", -// }, -// }); - -// const { otp } = getOTP(emails[0]); - -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "foo@example.com", -// }, -// }); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query: { -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// nonce, -// redirect_uri, -// response_type, -// scope, -// state, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - -// const accessToken = searchParams.get("access_token"); -// const accessTokenPayload = parseJwt(accessToken!); - -// // this is the id of the primary account -// expect(accessTokenPayload.sub).toBe("auth2|userId"); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); - -// expect(idTokenPayload.sub).toBe("auth2|userId"); - -// // ---------------------------- -// // now check the primary user has a new 'email' connection identity -// // ---------------------------- -// const primaryUserRes = await managementClient.users[":user_id"].$get( -// { -// param: { -// user_id: "auth2|userId", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); - -// const primaryUser = (await primaryUserRes.json()) as UserResponse; - -// expect(primaryUser.identities[1]).toMatchObject({ -// connection: "email", -// provider: "email", -// isSocial: false, -// profileData: { email: "foo@example.com", email_verified: true }, -// }); - -// // ---------------------------- -// // now check silent auth works when logged in with code -// // ---------------------------- - -// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; -// const { idToken: silentAuthIdTokenPayload } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader, -// oauthClient, -// nonce, -// "clientId", -// ); - -// // this is the id of the primary account -// expect(silentAuthIdTokenPayload.sub).toBe("auth2|userId"); - -// expect(silentAuthIdTokenPayload).toMatchObject({ -// aud: "clientId", -// email: "foo@example.com", -// email_verified: true, -// iss: "https://example.com/", -// name: "Åkesson Þorsteinsson", -// nickname: "Åkesson Þorsteinsson", -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// picture: "https://example.com/foo.png", -// }); - -// // ---------------------------- -// // now log in again with the same email and code user -// // ---------------------------- - -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: { -// nonce: "nonce", -// redirect_uri, -// response_type, -// scope, -// state, -// }, -// client_id: "clientId", -// connection: "email", -// email: "foo@example.com", -// send: "link", -// }, -// }); - -// const { otp: otp2 } = getOTP(emails[1]); - -// const authenticateResponse2 = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp: otp2, -// realm: "email", -// username: "foo@example.com", -// }, -// }); - -// const { login_ticket: loginTicket2 } = -// (await authenticateResponse2.json()) as { -// login_ticket: string; -// }; -// const tokenResponse2 = await oauthClient.authorize.$get( -// { -// query: { -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket: loginTicket2, -// nonce: "nonce", -// redirect_uri, -// response_type, -// scope, -// state, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// const accessToken2 = parseJwt( -// new URLSearchParams( -// tokenResponse2.headers.get("location")!.split("#")[1]!, -// ).get("access_token")!, -// ); - -// // this is the id of the primary account -// expect(accessToken2.sub).toBe("auth2|userId"); - -// // ---------------------------- -// // now check silent auth again! -// // ---------------------------- -// const setCookiesHeader2 = tokenResponse2.headers.get("set-cookie")!; -// const { idToken: silentAuthIdTokenPayload2 } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader2, -// oauthClient, -// nonce, -// "clientId", -// ); -// // second time round make sure we get the primary userid again -// expect(silentAuthIdTokenPayload2.sub).toBe("auth2|userId"); -// }); - -// describe("most complex linking flow I can think of", () => { -// it("should follow linked_to chain when logging in with new code user with same email address as existing username-password user THAT IS linked to a code user with a different email address", async () => { -// const token = await getAdminToken(); -// const { managementApp, oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); -// const managementClient = testClient(managementApp, env); - -// // ----------------- -// // create code user - the base user -// // ----------------- - -// await env.data.users.create("tenantId", { -// user_id: "email|the-base-user", -// email: "the-base-user@example.com", -// email_verified: true, -// login_count: 0, -// provider: "email", -// connection: "email", -// is_social: false, -// created_at: new Date().toISOString(), -// updated_at: new Date().toISOString(), -// }); - -// // ----------------- -// // create username-password user with different email address and link to the above user -// // ----------------- - -// await env.data.users.create("tenantId", { -// user_id: "auth2|the-auth2-same-email-user", -// email: "same-email@example.com", -// email_verified: true, -// login_count: 0, -// provider: "auth2", -// connection: "Username-Password-Authentication", -// is_social: false, -// created_at: new Date().toISOString(), -// updated_at: new Date().toISOString(), -// linked_to: "email|the-base-user", -// }); - -// // ----------------- -// // sanity check these users are linked -// // ----------------- - -// const baseUserRes = await managementClient.users[":user_id"].$get( -// { -// param: { -// user_id: "email|the-base-user", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); - -// const baseUser = (await baseUserRes.json()) as UserResponse; - -// expect(baseUser.identities).toEqual([ -// { -// connection: "email", -// provider: "email", -// user_id: "the-base-user", -// isSocial: false, -// }, -// { -// connection: "Username-Password-Authentication", -// provider: "auth2", -// user_id: "the-auth2-same-email-user", -// isSocial: false, -// profileData: { -// email: "same-email@example.com", -// email_verified: true, -// }, -// }, -// ]); - -// // ----------------- -// // Now do a new passwordless flow with a new user with email same-email@example.com -// // ----------------- - -// const passwordlessStartRes = await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "same-email@example.com", -// send: "code", -// }, -// }); -// expect(passwordlessStartRes.status).toBe(200); - -// const { otp } = getOTP(emails[0]); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "same-email@example.com", -// }, -// }); -// expect(authenticateResponse.status).toBe(200); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query: { -// ...AUTH_PARAMS, -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); -// const accessToken = searchParams.get("access_token"); -// const accessTokenPayload = parseJwt(accessToken!); - -// // this proves that we are following the linked user chain -// expect(accessTokenPayload.sub).toBe("email|the-base-user"); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); -// // this proves that we are following the linked user chain -// expect(idTokenPayload.email).toBe("the-base-user@example.com"); - -// // now check silent auth works when logged in with code---------------------------------------- -// const setCookiesHeader = tokenResponse.headers.get("set-cookie")!; - -// const { idToken: silentAuthIdTokenPayload } = -// await doSilentAuthRequestAndReturnTokens( -// setCookiesHeader, -// oauthClient, -// AUTH_PARAMS.nonce, -// "clientId", -// ); - -// // this proves the account linking chain is still working -// expect(silentAuthIdTokenPayload.sub).toBe("email|the-base-user"); - -// //------------------------------------------------------------------------------------------------ -// // fetch the base user again now and check we have THREE identities in there -// //------------------------------------------------------------------------------------------------ - -// const baseUserRes2 = await managementClient.users[":user_id"].$get( -// { -// param: { -// user_id: "email|the-base-user", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); - -// const baseUser2 = (await baseUserRes2.json()) as UserResponse; - -// expect(baseUser2.identities).toEqual([ -// { -// connection: "email", -// provider: "email", -// user_id: "the-base-user", -// isSocial: false, -// }, -// { -// connection: "Username-Password-Authentication", -// provider: "auth2", -// user_id: "the-auth2-same-email-user", -// isSocial: false, -// profileData: { -// email: "same-email@example.com", -// email_verified: true, -// }, -// }, -// { -// connection: "email", -// isSocial: false, -// profileData: { -// email: "same-email@example.com", -// email_verified: true, -// }, -// provider: "email", -// user_id: baseUser2.identities[2].user_id, -// }, -// ]); -// }); -// }); - -// it.skip("should only allow a code to be used once", async () => { -// const AUTH_PARAMS = { -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// redirect_uri: "https://login.example.com/callback", -// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, -// scope: "openid profile email", -// state: "state", -// }; - -// const { oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); - -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "foo@example.com", -// send: "code", -// }, -// }); - -// const { otp } = getOTP(emails[0]); - -// const authRes = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "foo@example.com", -// }, -// }); -// expect(authRes.status).toBe(200); - -// // now try to use the same code again -// const authRes2 = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "foo@example.com", -// }, -// }); - -// expect(authRes2.status).toBe(403); -// // this message isn't exactly true! We could check what auth0 does -// expect(await authRes2.json()).toEqual({ -// error: "access_denied", -// error_description: "Wrong email or verification code.", -// }); -// }); - -// it("should not accept an invalid code", async () => { -// const AUTH_PARAMS = { -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// redirect_uri: "https://login.example.com/callback", -// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, -// scope: "openid profile email", -// state: "state", -// }; - -// const { oauthApp, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); - -// await oauthClient.passwordless.start.$post( -// { -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "foo@example.com", -// send: "code", -// }, -// }, -// { -// headers: { -// "content-type": "application/json", -// }, -// }, -// ); - -// const BAD_CODE = "123456"; - -// const authRes = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp: BAD_CODE, -// realm: "email", -// username: "foo@example.com", -// }, -// }); - -// expect(authRes.status).toBe(403); -// }); - -// it("should be case insensitive with email address", async () => { -// const token = await getAdminToken(); -// const { managementApp, oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); -// const managementClient = testClient(managementApp, env); - -// // ------------------------- -// // Create new email user - all lower case email -// // ------------------------- -// const createUserResponse1 = await managementClient.users.$post( -// { -// json: { -// email: "john-doe@example.com", -// connection: "email", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); - -// expect(createUserResponse1.status).toBe(201); -// const newUser1 = (await createUserResponse1.json()) as UserResponse; -// expect(newUser1.email).toBe("john-doe@example.com"); - -// const AUTH_PARAMS = { -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// redirect_uri: "https://login.example.com/callback", -// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, -// scope: "openid profile email", -// state: "state", -// }; - -// // ----------------- -// // Sign in with same user passwordless -// // ----------------- -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// // do we want two tests? one for the username uppercase one for the domain? -// email: "JOHN-DOE@example.com", -// send: "code", -// }, -// }); - -// expect(emails.length).toBe(1); -// const { otp, to } = getOTP(emails[0]); -// expect(to).toBe("john-doe@example.com"); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "JOHN-DOE@example.com", -// }, -// }); - -// if (authenticateResponse.status !== 200) { -// throw new Error(await authenticateResponse.text()); -// } - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// const query = { -// ...AUTH_PARAMS, -// auth0client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// referrer: "https://login.example.com", -// realm: "email", -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get({ -// query, -// }); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); - -// expect(redirectUri.hostname).toBe("login.example.com"); - -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - -// expect(searchParams.get("state")).toBe("state"); - -// const accessToken = searchParams.get("access_token"); - -// const accessTokenPayload = parseJwt(accessToken!); -// expect(accessTokenPayload.sub).toBe(newUser1.user_id); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); -// expect(idTokenPayload.email).toBe("john-doe@example.com"); -// }); - -// it("should store new user email in lowercase", async () => { -// const { oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); - -// const AUTH_PARAMS = { -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// redirect_uri: "https://login.example.com/callback", -// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, -// scope: "openid profile email", -// state: "state", -// }; - -// // ----------------- -// // New passwordless sign up all uppercase - login2 would stop this... What does auth0.js do? CHECK! -// // ----------------- -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "JOHN-DOE@EXAMPLE.COM", -// send: "code", -// }, -// }); - -// const { otp } = getOTP(emails[0]); -// expect(otp).toBeTypeOf("string"); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// // use lowercase here... TBD -// username: "john-doe@example.com", -// }, -// }); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query: { -// ...AUTH_PARAMS, -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); -// const redirectUri = new URL(tokenResponse.headers.get("location")!); -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); -// const accessToken = searchParams.get("access_token"); - -// const sub = parseJwt(accessToken!).sub; - -// // this means we have created the user -// expect(tokenResponse.status).toBe(302); - -// // Now check in database we are storing in lower case - -// const newLowercaseUser = await env.data.users.get("tenantId", sub); - -// expect(newLowercaseUser!.email).toBe("john-doe@example.com"); -// }); - -// // TO TEST -// // - using expired codes? how can we fast-forward time with wrangler... -// // - more linked accounts -// // more basic error testing e.g. -// // - do not allow code from a different account: we should be fine without this but I can see a way we could mess this up! - -// describe("edge cases", () => { -// it("should login correctly for a code account linked to another account with a different email, when a password account has been registered but not verified", async () => { -// // create a new user with a password -// const token = await getAdminToken(); -// const { managementApp, oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); -// const managementClient = testClient(managementApp, env); - -// // ----------------- -// // user fixtures -// // ----------------- - -// // create new password user -// await env.data.users.create("tenantId", { -// user_id: "auth2|base-user", -// email: "base-user@example.com", -// email_verified: true, -// login_count: 0, -// provider: "auth2", -// connection: "Username-Password-Authentication", -// is_social: false, -// created_at: new Date().toISOString(), -// updated_at: new Date().toISOString(), -// }); -// // create new code user and link this to the password user -// await env.data.users.create("tenantId", { -// user_id: "auth2|code-user", -// email: "code-user@example.com", -// email_verified: true, -// login_count: 0, -// provider: "email", -// connection: "email", -// is_social: false, -// created_at: new Date().toISOString(), -// updated_at: new Date().toISOString(), -// linked_to: "auth2|base-user", -// }); - -// // sanity check - get base user and check identities -// const baseUserRes = await managementClient.users[":user_id"].$get( -// { -// param: { -// user_id: "auth2|base-user", -// }, -// header: { -// "tenant-id": "tenantId", -// }, -// }, -// { -// headers: { -// authorization: `Bearer ${token}`, -// }, -// }, -// ); -// expect(baseUserRes.status).toBe(200); -// const baseUser = (await baseUserRes.json()) as UserResponse; -// expect(baseUser.identities).toEqual([ -// { -// connection: "Username-Password-Authentication", -// isSocial: false, -// provider: "auth2", -// user_id: "base-user", -// }, -// { -// connection: "email", -// isSocial: false, -// profileData: { -// email: "code-user@example.com", -// email_verified: true, -// }, -// provider: "email", -// user_id: "code-user", -// }, -// ]); - -// // ----------------- -// // Now start password sign up with same code-user@example.com email -// // I'm seeing if this affects the code user with the same email address -// // ----------------- -// const createUserResponse = await oauthClient.dbconnections.signup.$post({ -// json: { -// client_id: "clientId", -// connection: "Username-Password-Authentication", -// email: "code-user@example.com", -// password: "Password1234!", -// }, -// }); - -// expect(createUserResponse.status).toBe(200); - -// //----------------- -// // now try and sign in with code-user@example.com code flow -// // I'm testing that the unlinked password user with the same email address does not affect this -// // ----------------- - -// const response = await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "code-user@example.com", -// send: "code", -// }, -// }); - -// expect(response.status).toBe(200); - -// // first email is email validation from sign up above -// const { otp } = getOTP(emails[1]); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "code-user@example.com", -// }, -// }); - -// if (authenticateResponse.status !== 200) { -// throw new Error( -// `Failed to authenticate with status: ${ -// authenticateResponse.status -// } and message: ${await response.text()}`, -// ); -// } - -// expect(authenticateResponse.status).toBe(200); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query: { -// ...AUTH_PARAMS, -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// expect(tokenResponse.status).toBe(302); -// expect(await tokenResponse.text()).toBe("Redirecting"); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); -// const accessToken = searchParams.get("access_token"); -// const accessTokenPayload = parseJwt(accessToken!); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); - -// // these prove that we are getting the code account's primary account! -// expect(accessTokenPayload.sub).toBe("auth2|base-user"); -// expect(idTokenPayload.email).toBe("base-user@example.com"); -// }); - -// it("should ignore un-verified password account when signing up with code account", async () => { -// const { oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); - -// // ----------------- -// // signup new user -// // ----------------- -// const createUserResponse = await oauthClient.dbconnections.signup.$post({ -// json: { -// client_id: "clientId", -// connection: "Username-Password-Authentication", -// email: "same-user-signin@example.com", -// password: "Password1234!", -// }, -// }); - -// expect(createUserResponse.status).toBe(200); - -// const unverifiedPasswordUser = await createUserResponse.json(); - -// //----------------- -// // sign up new code user that has same email address -// //----------------- -// const response = await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "same-user-signin@example.com", -// send: "code", -// }, -// }); - -// if (response.status !== 200) { -// throw new Error(await response.text()); -// } - -// // first email will be email verification -// const { otp } = getOTP(emails[1]); - -// // Authenticate using the code -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "same-user-signin@example.com", -// }, -// }); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// // Trade the ticket for token -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query: { -// ...AUTH_PARAMS, -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// realm: "email", -// }, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// expect(tokenResponse.status).toBe(302); -// expect(await tokenResponse.text()).toBe("Redirecting"); - -// const redirectUri = new URL(tokenResponse.headers.get("location")!); - -// const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - -// const accessToken = searchParams.get("access_token"); - -// const accessTokenPayload = parseJwt(accessToken!); -// expect(accessTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); - -// const idToken = searchParams.get("id_token"); -// const idTokenPayload = parseJwt(idToken!); -// expect(idTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); -// expect(idTokenPayload.email_verified).toBe(true); -// }); - -// // tickets are used by a few flows so this probably should not be here -// it("should only allow a ticket to be used once", async () => { -// const AUTH_PARAMS = { -// nonce: "ehiIoMV7yJCNbSEpRq513IQgSX7XvvBM", -// redirect_uri: "https://login.example.com/callback", -// response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, -// scope: "openid profile email", -// state: "state", -// }; -// const { oauthApp, emails, env } = await getTestServer(); -// const oauthClient = testClient(oauthApp, env); - -// await oauthClient.passwordless.start.$post({ -// json: { -// authParams: AUTH_PARAMS, -// client_id: "clientId", -// connection: "email", -// email: "foo@example.com", -// send: "code", -// }, -// }); -// const { otp } = getOTP(emails[0]); - -// const authenticateResponse = await oauthClient.co.authenticate.$post({ -// json: { -// client_id: "clientId", -// credential_type: "http://auth0.com/oauth/grant-type/passwordless/otp", -// otp, -// realm: "email", -// username: "foo@example.com", -// }, -// }); -// expect(authenticateResponse.status).toBe(200); - -// const { login_ticket } = (await authenticateResponse.json()) as { -// login_ticket: string; -// }; - -// // ----------------- -// // Trade the ticket for token once so it is used -// // ----------------- - -// const query = { -// ...AUTH_PARAMS, -// auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", -// client_id: "clientId", -// login_ticket, -// realm: "email", -// }; - -// const tokenResponse = await oauthClient.authorize.$get( -// { -// query, -// }, -// { -// headers: { -// referrer: "https://login.example.com", -// }, -// }, -// ); - -// expect(tokenResponse.status).toBe(302); -// expect(await tokenResponse.text()).toBe("Redirecting"); - -// // ----------------- -// // Now try trading ticket again and it should not work -// // ----------------- -// const rejectedSecondTicketUsageRes = await oauthClient.authorize.$get({ -// query, -// }); - -// expect(rejectedSecondTicketUsageRes.status).toBe(403); -// expect(await rejectedSecondTicketUsageRes.text()).toBe( -// "Ticket not found", -// ); -// }); -// }); -// }); diff --git a/test/integration/flows/magic-link-flow.spec.ts b/test/integration/flows/magic-link-flow.spec.ts deleted file mode 100644 index fe9b998b0..000000000 --- a/test/integration/flows/magic-link-flow.spec.ts +++ /dev/null @@ -1,724 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { parseJwt } from "../../../src/utils/parse-jwt"; -import { doSilentAuthRequestAndReturnTokens } from "../helpers/silent-auth"; -import { getTestServer } from "../helpers/test-server"; -import { getAdminToken } from "../helpers/token"; -import { testClient } from "hono/testing"; -import { EmailOptions } from "../../../src/services/email/EmailOptions"; -import { snapshotEmail } from "../helpers/playwrightSnapshots"; -import { z } from "zod"; -import { AuthorizationResponseType } from "authhero"; - -const AUTH_PARAMS = { - nonce: "enljIoQjQQy7l4pCVutpw9mf001nahBC", - redirect_uri: "https://login.example.com/callback", - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - scope: "openid profile email", - state: - "client_id=clientId&redirect_uri=https://example.com/callback&vendor_id=vendorId&connection=auth2", -}; - -function getMagicLinkFromEmailBody(email: EmailOptions) { - const linkEmailBody = email.content[0].value; - const magicLink = linkEmailBody.match( - /]*?\s+)?href="([^"]*)"/, - )![1]; - - return magicLink; -} - -const verifyCodeQuerySchema = z.object({ - scope: z.string(), - response_type: z.nativeEnum(AuthorizationResponseType), - redirect_uri: z.string(), - state: z.string(), - nonce: z.string(), - verification_code: z.string(), - connection: z.string(), - client_id: z.string(), - email: z.string(), - audience: z.string().optional(), -}); - -describe("magic link flow", () => { - describe("should log in using the sent magic link, when", () => { - it("is a new sign up", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ----------------- - // Doing a new signup here, so expect this email not to exist - // ----------------- - const resInitialQuery = await managementClient["users-by-email"].$get( - { - query: { - email: "new-user@example.com", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - }, - }, - ); - const results = await resInitialQuery.json(); - expect(results).toHaveLength(0); - - const response = await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "new-user@example.com", - send: "link", - }, - }); - - if (response.status !== 200) { - throw new Error(await response.text()); - } - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - await snapshotEmail(emails[0], true); - - expect(emails[0].to[0].email).toBe("new-user@example.com"); - - const link = magicLink!; - - const authenticatePath = link?.split("https://example.com")[1]; - - expect(authenticatePath).toContain("/passwordless/verify_redirect"); - - const querySearchParams = new URLSearchParams( - authenticatePath.split("?")[1], - ); - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(querySearchParams.entries()), - ); - - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - - if (authenticateResponse.status !== 302) { - const errorMessage = `Failed to verify redirect with status: ${ - authenticateResponse.status - } and message: ${await response.text()}`; - throw new Error(errorMessage); - } - - const redirectUri = new URL( - authenticateResponse.headers.get("location")!, - ); - expect(redirectUri.hostname).toBe("login.example.com"); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - expect(accessToken).toBeTypeOf("string"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe("openid profile email"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("new-user@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - - const authCookieHeader = authenticateResponse.headers.get("set-cookie")!; - - // now check silent auth works when logged in with magic link---------------------------------------- - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload.sub).toContain("email|"); - expect(silentAuthIdTokenPayload).toMatchObject({ - aud: "clientId", - name: "new-user@example.com", - email: "new-user@example.com", - email_verified: true, - nonce: "enljIoQjQQy7l4pCVutpw9mf001nahBC", - iss: "https://example.com/", - }); - }); - - it("is an existing primary user", async () => { - const token = await getAdminToken(); - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - - // ----------------- - // Create the user to log in with the magic link - // ----------------- - env.data.users.create("tenantId", { - user_id: "email|userId2", - email: "bar@example.com", - email_verified: true, - name: "", - nickname: "", - picture: "https://example.com/foo.png", - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }); - - const resInitialQuery = await managementClient["users-by-email"].$get( - { - query: { - email: "bar@example.com", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - "tenant-id": "tenantId", - }, - }, - ); - expect(resInitialQuery.status).toBe(200); - - // ----------------- - // Now get magic link emailed - // ----------------- - - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "bar@example.com", - send: "link", - }, - }); - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - expect(emails[0].to[0].email).toBe("bar@example.com"); - - const link = magicLink!; - - const authenticatePath = link?.split("https://example.com")[1]; - - expect(authenticatePath).toContain("/passwordless/verify_redirect"); - - const querySearchParams = new URLSearchParams( - authenticatePath.split("?")[1], - ); - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(querySearchParams.entries()), - ); - // ----------------- - // Authenticate using the magic link for the existing user - // ----------------- - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - - const redirectUri = new URL( - authenticateResponse.headers.get("location")!, - ); - expect(redirectUri.hostname).toBe("login.example.com"); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe("openid profile email"); - expect(accessTokenPayload.sub).toBe("email|userId2"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("bar@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - expect(idTokenPayload.sub).toBe("email|userId2"); - - const authCookieHeader = authenticateResponse.headers.get("set-cookie")!; - - // ---------------------------------------- - // now check silent auth works when logged in with magic link for existing user - // ---------------------------------------- - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload).toMatchObject({ - sub: "email|userId2", - aud: "clientId", - name: "", - nickname: "", - picture: "https://example.com/foo.png", - email: "bar@example.com", - email_verified: true, - nonce: "enljIoQjQQy7l4pCVutpw9mf001nahBC", - iss: "https://example.com/", - }); - }); - - it("is an existing linked user", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------------- - // Create the linked user to log in with the magic link - // ----------------- - env.data.users.create("tenantId", { - user_id: "auth2|userId2", - email: "foo@example.com", - email_verified: true, - name: "", - nickname: "", - picture: "https://example.com/foo.png", - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - linked_to: "auth2|userId", - }); - - // ----------------- - // Now get magic link emailed - // ----------------- - - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "link", - }, - }); - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - expect(emails[0].to[0].email).toBe("foo@example.com"); - - const link = magicLink!; - - const authenticatePath = link?.split("https://example.com")[1]; - - expect(authenticatePath).toContain("/passwordless/verify_redirect"); - - const querySearchParams = new URLSearchParams( - authenticatePath.split("?")[1], - ); - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(querySearchParams.entries()), - ); - // ----------------- - // Authenticate using the magic link for the existing user - // ----------------- - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - - const redirectUri = new URL( - authenticateResponse.headers.get("location")!, - ); - expect(redirectUri.hostname).toBe("login.example.com"); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe("openid profile email"); - // this id shows we are fetching the primary user - expect(accessTokenPayload.sub).toBe("auth2|userId"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("foo@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - expect(idTokenPayload.sub).toBe("auth2|userId"); - - const authCookieHeader = authenticateResponse.headers.get("set-cookie")!; - - // ---------------------------------------- - // now check silent auth works when logged in with magic link for existing user - // ---------------------------------------- - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload).toMatchObject({ - sub: "auth2|userId", - aud: "clientId", - name: "Åkesson Þorsteinsson", - nickname: "Åkesson Þorsteinsson", - picture: "https://example.com/foo.png", - email: "foo@example.com", - email_verified: true, - nonce: "enljIoQjQQy7l4pCVutpw9mf001nahBC", - iss: "https://example.com/", - }); - }); - - it("is the same email address as an existing password user", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------------- - // Now get magic link emailed - // ----------------- - - await oauthClient.passwordless.start.$post( - { - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "foo@example.com", - send: "link", - }, - }, - { - headers: { - "content-type": "application/json", - }, - }, - ); - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - expect(emails[0].to[0].email).toBe("foo@example.com"); - - const link = magicLink!; - - const authenticatePath = link?.split("https://example.com")[1]; - - expect(authenticatePath).toContain("/passwordless/verify_redirect"); - - const querySearchParams = new URLSearchParams( - authenticatePath.split("?")[1], - ); - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(querySearchParams.entries()), - ); - // ----------------- - // Authenticate using the magic link for the existing user - // ----------------- - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - - const redirectUri = new URL( - authenticateResponse.headers.get("location")!, - ); - expect(redirectUri.hostname).toBe("login.example.com"); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe("openid profile email"); - // this should we are fetching the primary user - expect(accessTokenPayload.sub).toBe("auth2|userId"); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("foo@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - expect(idTokenPayload.sub).toBe("auth2|userId"); - - const authCookieHeader = authenticateResponse.headers.get("set-cookie")!; - expect(authCookieHeader).toBeTypeOf("string"); - - // ---------------------------------------- - // now check silent auth works when logged in with magic link for existing user - // ---------------------------------------- - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - AUTH_PARAMS.nonce, - "clientId", - ); - - expect(silentAuthIdTokenPayload).toMatchObject({ - sub: "auth2|userId", - aud: "clientId", - name: "Åkesson Þorsteinsson", - nickname: "Åkesson Þorsteinsson", - picture: "https://example.com/foo.png", - email: "foo@example.com", - email_verified: true, - nonce: "enljIoQjQQy7l4pCVutpw9mf001nahBC", - iss: "https://example.com/", - }); - }); - }); - it.skip("should only allow a magic link to be used once", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------- - // get code to log in - // ----------- - await oauthClient.passwordless.start.$post( - { - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "test@example.com", - send: "link", - }, - }, - { - headers: { - "content-type": "application/json", - }, - }, - ); - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - const link = magicLink!; - - const authenticatePath = link?.split("https://example.com")[1]; - - expect(authenticatePath).toContain("/passwordless/verify_redirect"); - - const querySearchParams = new URLSearchParams( - authenticatePath.split("?")[1], - ); - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(querySearchParams.entries()), - ); - // ------------ - // Use the magic link - // ---------------- - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - expect(authenticateResponse.status).toBe(302); - // ------------ - // Try using the magic link twice - // ---------------- - const authenticateResponse2 = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - expect(authenticateResponse2.status).toBe(302); - const redirectUri2 = new URL( - authenticateResponse2.headers.get("location")!, - ); - expect(redirectUri2.hostname).toBe("login2.sesamy.dev"); - // we also show this page if the code is incorrect - expect(redirectUri2.pathname).toBe("/expired-code"); - }); - - it("should not accept an invalid code in the magic link", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------- - // get code to log in - // ----------- - await oauthClient.passwordless.start.$post( - { - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "test@example.com", - send: "link", - }, - }, - { - headers: { - "content-type": "application/json", - }, - }, - ); - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - const link = magicLink!; - // ------------ - // Overwrite the magic link with a bad code, and try and use it - // ---------------- - const magicLinkWithBadCode = new URL(link!); - magicLinkWithBadCode.searchParams.set("verification_code", "123456"); - - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(magicLinkWithBadCode.searchParams.entries()), - ); - - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - - // we are still getting a redirect but to a page on login2 saying the code is expired - expect(authenticateResponse.status).toBe(302); - const redirectUri = new URL(authenticateResponse.headers.get("location")!); - expect(redirectUri.hostname).toBe("login.example.com"); - expect(redirectUri.pathname).toBe("/callback"); - expect(redirectUri.searchParams.get("error")).toBe( - "Code not found or expired", - ); - }); - - it("should not accept a magic link where the email has been altered", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------- - // get code to log in - // ----------- - await oauthClient.passwordless.start.$post( - { - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "test@example.com", - send: "link", - }, - }, - { - headers: { - "content-type": "application/json", - }, - }, - ); - - const magicLink = getMagicLinkFromEmailBody(emails[0]); - - const link = magicLink!; - // ------------ - // Overwrite the magic link with a different email, and try and use it - // ---------------- - const magicLinkWithBadEmail = new URL(link!); - magicLinkWithBadEmail.searchParams.set("email", "another@email.com"); - - const authenticateResponse2 = - await oauthClient.passwordless.verify_redirect.$get({ - query: verifyCodeQuerySchema.parse( - Object.fromEntries(magicLinkWithBadEmail.searchParams.entries()), - ), - }); - expect(authenticateResponse2.status).toBe(302); - const redirectUri2 = new URL( - authenticateResponse2.headers.get("location")!, - ); - expect(redirectUri2.hostname).toBe("example.com"); - expect(redirectUri2.pathname).toBe("/u/invalid-session"); - }); - - describe("edge cases", () => { - it("should ignore un-verified password account when signing up with magic link", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - // ----------------- - // signup new user - // ----------------- - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "same-user-signin@example.com", - password: "Password1234!", - }, - }); - expect(createUserResponse.status).toBe(200); - - const unverifiedPasswordUser = await createUserResponse.json(); - - //----------------- - // sign up new code user that has same email address - //----------------- - await oauthClient.passwordless.start.$post({ - json: { - authParams: AUTH_PARAMS, - client_id: "clientId", - connection: "email", - email: "same-user-signin@example.com", - send: "link", - }, - }); - - const magicLink = getMagicLinkFromEmailBody(emails[1]); - - const authenticatePath = magicLink?.split("https://example.com")[1]; - - const querySearchParams = new URLSearchParams( - authenticatePath.split("?")[1], - ); - const query = verifyCodeQuerySchema.parse( - Object.fromEntries(querySearchParams.entries()), - ); - - const authenticateResponse = - await oauthClient.passwordless.verify_redirect.$get({ - query, - }); - expect(authenticateResponse.status).toBe(302); - - const redirectUri = new URL( - authenticateResponse.headers.get("location")!, - ); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const accessToken = searchParams.get("access_token"); - - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.sub).not.toBe(unverifiedPasswordUser._id); - expect(idTokenPayload.email_verified).toBe(true); - }); - }); -}); -// TO TEST -// - should we do silent auth after each of these calls? diff --git a/test/integration/flows/password.spec.ts b/test/integration/flows/password.spec.ts deleted file mode 100644 index 88dd9036f..000000000 --- a/test/integration/flows/password.spec.ts +++ /dev/null @@ -1,1098 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { parseJwt } from "../../../src/utils/parse-jwt"; -import { doSilentAuthRequestAndReturnTokens } from "../helpers/silent-auth"; -import { getTestServer } from "../helpers/test-server"; -import { testClient } from "hono/testing"; -import { getAdminToken } from "../helpers/token"; -import type { EmailOptions } from "../../../src/services/email/EmailOptions"; -import { - snapshotResponse, - snapshotEmail, -} from "../helpers/playwrightSnapshots"; -import { - AuthorizationResponseType, - Log, - LogTypes, - User, - UserResponse, -} from "authhero"; - -function getCodeStateTo(email: EmailOptions) { - const verifyEmailBody = email.content[0].value; - // this gets the space before so we don't match CSS colours - const codes = verifyEmailBody.match(/(?!#).[0-9]{6}/g)!; - - const code = codes[0].slice(1); - - const to = email.to[0].email; - - // this is a param on the verify email magic link - const state = verifyEmailBody.match(/state=([^&]+)/)![1]; - - return { code, state, to }; -} - -describe("password-flow", () => { - describe("Register password", () => { - it("should return a 403 if an invalid client is passed", async () => { - const { oauthApp, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const response = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "invalidClientId", - connection: "Username-Password-Authentication", - email: "test@example.com", - password: "Password1234!", - }, - }); - expect(await response.text()).toBe("Client not found"); - - expect(response.status).toBe(403); - }); - - it("should create a new user with a password and only allow login after email validation", async () => { - const password = "Password1234!"; - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "password-login-test@example.com", - password, - }, - }); - expect(createUserResponse.status).toBe(200); - - const { - logs: [successSignUpLog], - } = await env.data.logs.list("tenantId", { - page: 0, - per_page: 100, - include_totals: true, - }); - expect(successSignUpLog).toMatchObject({ - type: "ss", - tenant_id: "tenantId", - user_name: "password-login-test@example.com", - connection: "Username-Password-Authentication", - client_id: "clientId", - }); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password, - username: "password-login-test@example.com", - }, - }); - - // this will not work! need to validate the email before allowing a login - expect(loginResponse.status).toBe(403); - - // get user with this id and check is the correct id - const user = await env.data.users.get( - "tenantId", - successSignUpLog.user_id!, - ); - - expect(user).toMatchObject({ - email: "password-login-test@example.com", - }); - - // this is the original email sent after signing up - const { to, code, state } = getCodeStateTo(emails[0]); - - await snapshotEmail(emails[0]); - - expect(to).toBe("password-login-test@example.com"); - expect(code).toBeDefined(); - expect(state).toBeTypeOf("string"); - - const emailValidatedRes = await oauthClient.u["validate-email"].$get({ - query: { - state, - code, - }, - }); - expect(emailValidatedRes.status).toBe(200); - - //------------------- - // login again now to check that it works - //------------------- - - const loginResponse2 = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password, - username: "password-login-test@example.com", - }, - }); - - const { login_ticket: loginTicket2 } = (await loginResponse2.json()) as { - login_ticket: string; - }; - - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket: loginTicket2, - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - redirect_uri: "http://login.example.com", - state: "state", - realm: "Username-Password-Authentication", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - expect(redirectUri.hostname).toBe("login.example.com"); - expect(searchParams.get("state")).toBe("state"); - const accessToken = searchParams.get("access_token"); - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe(""); - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("password-login-test@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - - const authCookieHeader = tokenResponse.headers.get("set-cookie")!; - // now check silent auth works after password login - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - "unique-nonce", - "clientId", - ); - - expect(silentAuthIdTokenPayload.sub).toContain("auth2|"); - expect(silentAuthIdTokenPayload).toMatchObject({ - aud: "clientId", - email: "password-login-test@example.com", - email_verified: true, - nonce: "unique-nonce", - iss: "https://example.com/", - }); - - // Validate the logs - const { logs } = await env.data.logs.list("tenantId", { - page: 0, - per_page: 100, - include_totals: true, - }); - - const failedLoginLog = logs.find( - (log: Log) => log.type === LogTypes.FAILED_LOGIN, - ); - - expect(failedLoginLog).toMatchObject({ - type: "f", - tenant_id: "tenantId", - user_name: "password-login-test@example.com", - connection: "Username-Password-Authentication", - client_id: "clientId", - description: "Email not verified", - }); - - const silentAuthSuccessLog = logs.find( - (log: Log) => log.type === LogTypes.SUCCESS_SILENT_AUTH, - ); - - expect(silentAuthSuccessLog).toMatchObject({ - type: "ssa", - tenant_id: "tenantId", - user_id: accessTokenPayload.sub, - user_name: "password-login-test@example.com", - connection: "Username-Password-Authentication", - description: "Successful silent authentication", - }); - - const sucessfulCrossOriginAuthentictationLog = logs.find( - (log: Log) => log.type === LogTypes.SUCCESS_CROSS_ORIGIN_AUTHENTICATION, - ); - - expect(sucessfulCrossOriginAuthentictationLog).toMatchObject({ - type: "scoa", - tenant_id: "tenantId", - user_id: accessTokenPayload.sub, - user_name: "password-login-test@example.com", - connection: "Username-Password-Authentication", - }); - }); - - // maybe this test should be broken up into login tests below... maybe we want more flows like this! - // still more to test e.g. resent email validation email after failed login (here we are just testing the verify email email which is only sent once) - it("should create a new user with a password, only allow login after email validation AND link this to an existing code user with the same email", async () => { - const password = "Password1234!"; - const { managementApp, oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - const managementClient = testClient(managementApp, env); - const token = await getAdminToken(); - - // ------------------------------- - // create code user - // ------------------------------- - await env.data.users.create("tenantId", { - user_id: "email|codeUserId", - email: "existing-code-user@example.com", - email_verified: true, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - login_count: 0, - }); - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "existing-code-user@example.com", - password, - }, - }); - expect(createUserResponse.status).toBe(200); - - // ----------------------------- - // validate email - // ----------------------------- - const { to, code, state } = getCodeStateTo(emails[0]); - - expect(to).toBe("existing-code-user@example.com"); - expect(code).toBeDefined(); - - const emailValidatedRes = await oauthClient.u["validate-email"].$get({ - query: { - state, - code, - }, - }); - - expect(emailValidatedRes.status).toBe(200); - - // ----------------------------- - // sanity check that linking has happened! - // ----------------------------- - const users = await env.data.users.list("tenantId", { - page: 0, - per_page: 10, - include_totals: false, - q: "", - }); - const [linkedPasswordUser] = users.users.filter( - (u: User) => - u.email === "existing-code-user@example.com" && - u.provider === "auth2", - ); - - expect(linkedPasswordUser.linked_to).toBe("email|codeUserId"); - - // ----------------------------- - // login with password - // ----------------------------- - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password, - username: "existing-code-user@example.com", - }, - }); - - expect(loginResponse.status).toBe(200); - - const { login_ticket } = (await loginResponse.json()) as { - login_ticket: string; - }; - - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - redirect_uri: "http://login.example.com", - state: "state", - realm: "Username-Password-Authentication", - }, - }, - { - headers: { - referrer: "https://login.example.com", - "cf-connecting-ip": "1.2.3.4", - }, - }, - ); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - expect(redirectUri.hostname).toBe("login.example.com"); - expect(searchParams.get("state")).toBe("state"); - const idTokenPayload = parseJwt(searchParams.get("id_token")!); - expect(idTokenPayload.email).toBe("existing-code-user@example.com"); - expect(idTokenPayload.sub).toBe("email|codeUserId"); - const authCookieHeader = tokenResponse.headers.get("set-cookie")!; - // now check silent auth works after password login - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - "unique-nonce", - "clientId", - ); - - // this proves that account linking has happened - expect(silentAuthIdTokenPayload.sub).toBe("email|codeUserId"); - - // ----------------------------- - // get user by id assert that the username-password user info is in the identities array - // -------------------- - const primaryUserRes = await managementClient.users[":user_id"].$get( - { - param: { - user_id: "email|codeUserId", - }, - header: { - "tenant-id": "tenantId", - }, - }, - { - headers: { - authorization: `Bearer ${token}`, - "tenant-id": "tenantId", - }, - }, - ); - - const primaryUser = (await primaryUserRes.json()) as UserResponse; - - expect(primaryUser.identities).toEqual([ - { - connection: "email", - provider: "email", - user_id: "codeUserId", - isSocial: false, - }, - { - connection: "Username-Password-Authentication", - provider: "auth2", - user_id: primaryUser.identities[1].user_id, - isSocial: false, - profileData: { - email: "existing-code-user@example.com", - email_verified: true, - }, - }, - ]); - - // Check that the login count and last IP has been updated - expect(primaryUser.login_count).toBe(2); - expect(primaryUser.last_ip).toBe("1.2.3.4"); - - const lastLogin = new Date(primaryUser.last_login!); - expect(Date.now() - lastLogin.getTime()).lessThan(1000); - }); - - it("should resend email validation email after login attempts, and this should work", async () => { - const password = "Password1234!"; - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "password-login-test@example.com", - password, - }, - }); - expect(createUserResponse.status).toBe(200); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password, - username: "password-login-test@example.com", - }, - }); - - // --------------------------- - // this will not work because email not validated - // --------------------------- - expect(loginResponse.status).toBe(403); - - // this is the difference to the previous test - we are using the verified email that is sent after a failed login - // either of these two emails would work - const { to, code, state } = getCodeStateTo(emails[1]); - - expect(to).toBe("password-login-test@example.com"); - - const emailValidatedRes = await oauthClient.u["validate-email"].$get({ - query: { - state, - code, - }, - }); - - expect(emailValidatedRes.status).toBe(200); - - // ----------------------------------------- - // do the login flow again - // ----------------------------------------- - - const loginResponse2 = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password, - username: "password-login-test@example.com", - }, - }); - - const { login_ticket: loginTicket2 } = (await loginResponse2.json()) as { - login_ticket: string; - }; - - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket: loginTicket2, - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - redirect_uri: "http://login.example.com", - state: "state", - realm: "Username-Password-Authentication", - }, - }, - { - headers: { referrer: "https://login.example.com" }, - }, - ); - - expect(tokenResponse.status).toBe(302); - const redirectUri = new URL(tokenResponse.headers.get("location")!); - - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("password-login-test@example.com"); - }); - - it("should not allow a new sign up to overwrite the password of an existing signup", async () => { - const { oauthApp, env } = await getTestServer(); - const aNewPassword = "A-new-valid-password-1234!"; - const oauthClient = testClient(oauthApp, env); - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - // existing password user in our fixtures - email: "foo@example.com", - password: aNewPassword, - }, - }); - - expect(createUserResponse.status).toBe(400); - const body = await createUserResponse.text(); - expect(body).toBe("Invalid sign up"); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: aNewPassword, - username: "foo@example.com", - }, - }); - expect(loginResponse.status).toBe(403); - }); - it("should reject signups for weak passwords", async () => { - const { oauthApp, env } = await getTestServer(); - - const oauthClient = testClient(oauthApp, env); - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "weak-password@example.com", - password: "password", - }, - }); - - expect(createUserResponse.status).toBe(400); - }); - // TO TEST-------------------------------------------------------- - // should do what with registration signup for existing email (code) user? - // --- we don't have account linking implemented on this flow - // same username-password user but a different tenant - }); - describe("Login with password", () => { - it("should login with existing user", async () => { - const { oauthApp, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - // foo@example.com is an existing username-password user, with password - Test! - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "Test1234!", - username: "foo@example.com", - }, - }); - - expect(loginResponse.status).toBe(200); - const { login_ticket } = (await loginResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - redirect_uri: "http://login.example.com", - state: "state", - realm: "Username-Password-Authentication", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - expect(tokenResponse.status).toBe(302); - expect(await tokenResponse.text()).toBe("Redirecting"); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - expect(redirectUri.hostname).toBe("login.example.com"); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - expect(searchParams.get("state")).toBe("state"); - - const accessToken = searchParams.get("access_token"); - const accessTokenPayload = parseJwt(accessToken!); - expect(accessTokenPayload.aud).toBe("default"); - expect(accessTokenPayload.iss).toBe("https://example.com/"); - expect(accessTokenPayload.scope).toBe(""); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("foo@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - - const authCookieHeader = tokenResponse.headers.get("set-cookie")!; - - // ------------------ - // now check silent auth works after password login with existing user - // ------------------ - const { idToken: silentAuthIdTokenPayload } = - await doSilentAuthRequestAndReturnTokens( - authCookieHeader, - oauthClient, - "unique-nonce", - "clientId", - ); - expect(silentAuthIdTokenPayload).toMatchObject({ - sub: "auth2|userId", - aud: "clientId", - email: "foo@example.com", - email_verified: true, - nonce: "unique-nonce", - iss: "https://example.com/", - name: "Åkesson Þorsteinsson", - nickname: "Åkesson Þorsteinsson", - picture: "https://example.com/foo.png", - }); - }); - - it("should reject login of existing user with incorrect password", async () => { - const { oauthApp, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "wrong-password", - username: "foo@example.com", - }, - }); - // no body returned - expect(loginResponse.status).toBe(403); - }); - - it("should not allow password of a different user to be used", async () => { - const { oauthApp, env } = await getTestServer({ - emailValidation: "disabled", - }); - const oauthClient = testClient(oauthApp, env); - - const signupResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "new-username-password-user@example.com", - password: "Password1234!", - }, - }); - expect(signupResponse.status).toBe(200); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "Password1234!", - username: "new-username-password-user@example.com", - }, - }); - - // ------------------ - // is this enough to be sure the user is created? OR should we exchange the ticket... - // or is just calling /dbconnection/register enough? - // ------------------ - expect(loginResponse.status).toBe(200); - // ------------------ - // now check we cannot use the wrong user's password - // ------------------ - - const rejectedLoginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - // this is the password of foo@example.com - password: "Test1234!", - username: "new-username-password-user@example.com", - }, - }); - - expect(rejectedLoginResponse.status).toBe(403); - }); - - it("should not allow non-existent user & password to login", async () => { - const { oauthApp, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "any-password", - username: "non-existent-user@example.com", - }, - }); - expect(loginResponse.status).toBe(403); - }); - - it("should not allow login to username-password but on different tenant", async () => { - const { oauthApp, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "otherClientIdOnOtherTenant", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "Test1234!", - username: "foo@example.com", - }, - }); - - expect(loginResponse.status).toBe(403); - }); - // TO TEST - // - username-password user across different clients on the same tenant - // - username-password user existing on two different tenants, but with different passwords... then check each doesn't work on the other - }); - - describe("Password reset", () => { - it("should send password reset email for existing user, and allow password to be changed", async () => { - const { oauthApp, emails, env } = await getTestServer({ - // emails are based on tenant styling... I'm surprised we don't already have bugs - // vendor_id: "fokus", - testTenantLanguage: "sv", - }); - const oauthClient = testClient(oauthApp, env); - - // foo@example.com is an existing username-password user - // with password - Test! - - //------------------- - // send password reset email - //------------------- - - const passwordResetSendResponse = - await oauthClient.dbconnections.change_password.$post({ - json: { - client_id: "clientId", - email: "foo@example.com", - connection: "Username-Password-Authentication", - }, - }); - expect(passwordResetSendResponse.status).toBe(200); - expect(await passwordResetSendResponse.text()).toBe( - "We've just sent you an email to reset your password.", - ); - - const { to, code, state } = getCodeStateTo(emails[0]); - - expect(to).toBe("foo@example.com"); - expect(code).toBeDefined(); - expect(state).toBeDefined(); - - await snapshotEmail(emails[0]); - - //------------------- - // reset password - //------------------- - - const resetPasswordForm = await oauthClient.u["reset-password"].$get({ - query: { - state, - code, - }, - }); - - await snapshotResponse(resetPasswordForm); - - // NOTE - I'm not testing the GET that loads the webform here... we don't have a browser to interact with here - const resetPassword = await oauthClient.u["reset-password"].$post({ - form: { - password: "New-password-1234!", - "re-enter-password": "New-password-1234!", - }, - query: { - code, - state, - }, - }); - - expect(resetPassword.status).toBe(200); - - await snapshotResponse(resetPassword); - - // ------------------ - // now check we can login with the new password - // ------------------ - - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "New-password-1234!", - username: "foo@example.com", - }, - }); - - expect(loginResponse.status).toBe(200); - const { login_ticket } = (await loginResponse.json()) as { - login_ticket: string; - }; - - // Trade the ticket for token - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - redirect_uri: "http://login.example.com", - state: "state", - realm: "Username-Password-Authentication", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - const redirectUri = new URL(tokenResponse.headers.get("location")!); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("foo@example.com"); - expect(idTokenPayload.aud).toBe("clientId"); - }); - - it("should send password reset email for users with a matching email but no password user", async () => { - const { oauthApp, emails, env } = await getTestServer({ - testTenantLanguage: "sv", - }); - const oauthClient = testClient(oauthApp, env); - - await env.data.users.create("tenantId", { - user_id: "email|userId", - email: "test@example.com", - email_verified: true, - name: "test", - login_count: 0, - provider: "email", - connection: "email", - is_social: false, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), - }); - - //------------------- - // send password reset email - //------------------- - const passwordResetSendResponse = - await oauthClient.dbconnections.change_password.$post({ - json: { - client_id: "clientId", - email: "test@example.com", - connection: "Username-Password-Authentication", - }, - }); - expect(passwordResetSendResponse.status).toBe(200); - expect(await passwordResetSendResponse.text()).toBe( - "We've just sent you an email to reset your password.", - ); - - const { to, code, state } = getCodeStateTo(emails[0]); - - expect(to).toBe("test@example.com"); - expect(code).toBeDefined(); - expect(state).toBeDefined(); - }); - - it("should reject weak passwords", async () => { - const { oauthApp, env, emails } = await getTestServer({ - // vendor_id: "kvartal", - testTenantLanguage: "nb", - }); - const oauthClient = testClient(oauthApp, env); - - // foo@example.com is an existing username-password user - // with password - Test! - - //------------------- - // get code to call password reset endpoint - //------------------- - await oauthClient.dbconnections.change_password.$post({ - json: { - client_id: "clientId", - email: "foo@example.com", - connection: "Username-Password-Authentication", - }, - }); - const { code, state } = getCodeStateTo(emails[0]); - - //------------------- - // reject when try to set weak password - //------------------- - const resetPassword = await oauthClient.u["reset-password"].$post({ - form: { - // we have unit tests for the util function we use so just doing one unhappy path - password: "weak-password", - "re-enter-password": "weak-password", - }, - query: { - code, - state, - }, - }); - - expect(resetPassword.status).toBe(400); - - await snapshotResponse(resetPassword); - }); - it("should reject non-matching confirmation password", async () => { - const { oauthApp, env, emails } = await getTestServer({ - // vendor_id: "breakit", - testTenantLanguage: "it", - }); - - const oauthClient = testClient(oauthApp, env); - - // foo@example.com is an existing username-password user - // with password - Test! - - //------------------- - // get code to call password reset endpoint - //------------------- - await oauthClient.dbconnections.change_password.$post({ - json: { - client_id: "clientId", - email: "foo@example.com", - connection: "Username-Password-Authentication", - }, - }); - const { code, state } = getCodeStateTo(emails[0]); - - //------------------- - // reject when confrimation password does not match! - //------------------- - const resetPassword = await oauthClient.u["reset-password"].$post({ - form: { - password: "StrongPassword1234!", - // this is also strong but does match the previous line - "re-enter-password": "AnotherStrongPassword1234!", - }, - query: { - state, - code, - }, - }); - - expect(resetPassword.status).toBe(400); - - await snapshotResponse(resetPassword); - }); - it("should send password reset email for new unvalidated signup AND set email_verified to true", async () => { - const { oauthApp, emails, env } = await getTestServer(); - const oauthClient = testClient(oauthApp, env); - - const createUserResponse = await oauthClient.dbconnections.signup.$post({ - json: { - client_id: "clientId", - connection: "Username-Password-Authentication", - email: "reset-new-user@example.com", - password: "Password1234!", - }, - }); - expect(createUserResponse.status).toBe(200); - - //------------------- - // send password reset email even though have never logged in - //------------------- - const passwordResetSendResponse = - await oauthClient.dbconnections.change_password.$post({ - json: { - client_id: "clientId", - email: "reset-new-user@example.com", - connection: "Username-Password-Authentication", - }, - }); - expect(passwordResetSendResponse.status).toBe(200); - expect(await passwordResetSendResponse.text()).toBe( - "We've just sent you an email to reset your password.", - ); - - // first email is the verify email email sent after sign up - const { to, code, state } = getCodeStateTo(emails[1]); - - expect(to).toBe("reset-new-user@example.com"); - expect(code).toBeDefined(); - expect(state).toBeDefined(); - //------------------- - // reset password - //------------------- - const resetPassword = await oauthClient.u["reset-password"].$post({ - form: { - password: "New-password-1234!", - "re-enter-password": "New-password-1234!", - }, - query: { - state, - code, - }, - }); - expect(resetPassword.status).toBe(200); - - // ------------------ - // now check we can login with the new password, and we are not told to verify our email - // ------------------ - const loginResponse = await oauthClient.co.authenticate.$post({ - json: { - client_id: "clientId", - credential_type: "http://auth0.com/oauth/grant-type/password-realm", - realm: "Username-Password-Authentication", - password: "New-password-1234!", - username: "reset-new-user@example.com", - }, - }); - expect(loginResponse.status).toBe(200); - const { login_ticket } = (await loginResponse.json()) as { - login_ticket: string; - }; - const tokenResponse = await oauthClient.authorize.$get( - { - query: { - auth0Client: "eyJuYW1lIjoiYXV0aDAuanMiLCJ2ZXJzaW9uIjoiOS4yMy4wIn0=", - client_id: "clientId", - login_ticket, - response_type: AuthorizationResponseType.TOKEN_ID_TOKEN, - redirect_uri: "http://login.example.com", - state: "state", - realm: "Username-Password-Authentication", - }, - }, - { - headers: { - referrer: "https://login.example.com", - }, - }, - ); - - // this proves that email_verified is set to true, and the new password has been set - expect(tokenResponse.status).toBe(302); - const redirectUri = new URL(tokenResponse.headers.get("location")!); - const searchParams = new URLSearchParams(redirectUri.hash.slice(1)); - const accessToken = searchParams.get("access_token"); - expect(accessToken).toBeDefined(); - - const idToken = searchParams.get("id_token"); - const idTokenPayload = parseJwt(idToken!); - expect(idTokenPayload.email).toBe("reset-new-user@example.com"); - expect(idTokenPayload.email_verified).toBe(true); - }); - }); - - // TO TEST - // link a code user to another user with a different email address - // THEN do an email password sign up with this same email address - // I don't think this code will follow the chain of linked accounts... how complex could this get? -}); diff --git a/test/integration/flows/social.spec.ts b/test/integration/flows/social.spec.ts index c2c427aeb..2ca500e7c 100644 --- a/test/integration/flows/social.spec.ts +++ b/test/integration/flows/social.spec.ts @@ -697,14 +697,13 @@ describe("social sign on", () => { // ----------------- // create new user // ----------------- - await env.data.users.create("tenantId", { email: "örjan.lindström@example.com", email_verified: false, name: "örjan", - provider: "email", + provider: "auth2", connection: "Username-Password-Authentication", - user_id: "email|123456789012345678901", + user_id: "auth2|123456789012345678901", last_ip: "", login_count: 0, is_social: false, From e340071e2bf0155d6b1624030c78f899d500f012 Mon Sep 17 00:00:00 2001 From: Markus Ahlstrand Date: Tue, 17 Dec 2024 09:58:19 +0100 Subject: [PATCH 3/4] chore: ignore type issue --- src/components/Button.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 4488ae646..01aafc145 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -28,6 +28,7 @@ const Button = ({ }: PropsWithChildren) => { const hrefProps = Component === "a" ? { href } : {}; return ( + // @ts-expect-error - refactor this when migrating to authhero Date: Tue, 17 Dec 2024 10:13:27 +0100 Subject: [PATCH 4/4] fix: revert playwrigt --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 21a161151..66f5c84c8 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "liquidjs": "^10.19.0", "nanoid": "5.0.9", "oslo": "^1.2.1", - "playwright": "1.49.1", + "playwright": "1.44.1", "zod": "3.24.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 9fa4dd93c..701393c06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6532,17 +6532,17 @@ pkg-conf@^2.1.0: find-up "^2.0.0" load-json-file "^4.0.0" -playwright-core@1.49.1: - version "1.49.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015" - integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg== +playwright-core@1.44.1: + version "1.44.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.44.1.tgz#53ec975503b763af6fc1a7aa995f34bc09ff447c" + integrity sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA== -playwright@1.49.1: - version "1.49.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c" - integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA== +playwright@1.44.1: + version "1.44.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.44.1.tgz#5634369d777111c1eea9180430b7a184028e7892" + integrity sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg== dependencies: - playwright-core "1.49.1" + playwright-core "1.44.1" optionalDependencies: fsevents "2.3.2"