From 6e7c82cc198487f6f04ecd27bd3552ee11dbf437 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 31 Dec 2024 14:32:24 +0100 Subject: [PATCH] split invite, email verification --- apps/login/cypress/integration/invite.cy.ts | 105 ++++++++++++++++++++ apps/login/cypress/integration/login.cy.ts | 1 + apps/login/cypress/integration/verify.cy.ts | 89 ++--------------- apps/login/src/app/(login)/verify/page.tsx | 16 +-- apps/login/src/lib/server/loginname.ts | 1 + apps/login/src/lib/server/verify.ts | 11 +- 6 files changed, 135 insertions(+), 88 deletions(-) create mode 100644 apps/login/cypress/integration/invite.cy.ts diff --git a/apps/login/cypress/integration/invite.cy.ts b/apps/login/cypress/integration/invite.cy.ts new file mode 100644 index 00000000000..5b343dccabe --- /dev/null +++ b/apps/login/cypress/integration/invite.cy.ts @@ -0,0 +1,105 @@ +import { stub } from "../support/mock"; + +describe("verify invite", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [], // user with no auth methods was invited + }, + }); + + stub("zitadel.user.v2.UserService", "GetUserByID", { + data: { + user: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: true, // email needs to be verified + }, + }, + }, + }, + }); + + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: + "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); + }); + + it.only("shows authenticators after successful invite verification", () => { + stub("zitadel.user.v2.UserService", "VerifyInviteCode"); + + cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); + cy.location("pathname", { timeout: 10_000 }).should( + "eq", + "/authenticator/set", + ); + }); + + it("shows an error if invite code validation failed", () => { + stub("zitadel.user.v2.UserService", "VerifyInviteCode", { + code: 3, + error: "error validating code", + }); + + // TODO: Avoid uncaught exception in application + cy.once("uncaught:exception", () => false); + cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); + cy.contains("Could not verify invite", { timeout: 10_000 }); + }); +}); diff --git a/apps/login/cypress/integration/login.cy.ts b/apps/login/cypress/integration/login.cy.ts index a03863a9ce1..3e74c0f7fec 100644 --- a/apps/login/cypress/integration/login.cy.ts +++ b/apps/login/cypress/integration/login.cy.ts @@ -165,6 +165,7 @@ describe("login", () => { }, }); }); + it("should redirect a user with passwordless authentication to /passkey", () => { cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); cy.location("pathname", { timeout: 10_000 }).should("eq", "/passkey"); diff --git a/apps/login/cypress/integration/verify.cy.ts b/apps/login/cypress/integration/verify.cy.ts index 7c28350e527..c4e173d25fb 100644 --- a/apps/login/cypress/integration/verify.cy.ts +++ b/apps/login/cypress/integration/verify.cy.ts @@ -1,6 +1,6 @@ import { stub } from "../support/mock"; -describe("verify invite", () => { +describe("verify email", () => { beforeEach(() => { stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { data: { @@ -13,7 +13,7 @@ describe("verify invite", () => { stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { data: { - authMethodTypes: [], + authMethodTypes: [1], // set one method such that we know that the user was not invited }, }); @@ -83,83 +83,16 @@ describe("verify invite", () => { }); }); - it.only("shows authenticators after successful invite verification", () => { - stub("zitadel.user.v2.UserService", "GetUserByID", { - data: { - user: { - userId: "221394658884845598", - state: 1, - username: "john@zitadel.com", - loginNames: ["john@zitadel.com"], - preferredLoginName: "john@zitadel.com", - human: { - userId: "221394658884845598", - state: 1, - username: "john@zitadel.com", - loginNames: ["john@zitadel.com"], - preferredLoginName: "john@zitadel.com", - profile: { - givenName: "John", - familyName: "Doe", - avatarUrl: "https://zitadel.com/avatar.jpg", - }, - email: { - email: "john@zitadel.com", - isVerified: true, // email needs to be verified - }, - }, - }, - }, - }); + // it("shows password and passkey method after successful invite verification", () => { + // stub("zitadel.user.v2.UserService", "VerifyEmail"); + // cy.visit("/verify?userId=221394658884845598&code=abc"); + // cy.location("pathname", { timeout: 10_000 }).should( + // "eq", + // "/authenticator/set", + // ); + // }); - stub("zitadel.user.v2.UserService", "VerifyInviteCode"); - cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); - cy.location("pathname", { timeout: 10_000 }).should( - "eq", - "/authenticator/set", - ); - }); - - it("shows an error if invite code validation failed", () => { - stub("zitadel.user.v2.UserService", "VerifyInviteCode", { - code: 3, - error: "error validating code", - }); - // TODO: Avoid uncaught exception in application - cy.once("uncaught:exception", () => false); - cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); - cy.contains("Could not verify invite", { timeout: 10_000 }); - }); -}); - -describe("verify email", () => { - beforeEach(() => { - stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { - data: { - details: { - totalResult: 1, - }, - result: [{ id: "256088834543534543" }], - }, - }); - - stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { - data: { - authMethodTypes: [], - }, - }); - }); - - it("shows password and passkey method after successful invite verification", () => { - stub("zitadel.user.v2.UserService", "VerifyEmail"); - cy.visit("/verify?userId=221394658884845598&code=abc"); - cy.location("pathname", { timeout: 10_000 }).should( - "eq", - "/authenticator/set", - ); - }); - - it("shows an error if invite code validation failed", () => { + it("shows an error if email code validation failed", () => { stub("zitadel.user.v2.UserService", "VerifyEmail", { code: 3, error: "error validating code", diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index ca007a22f4f..959998c814a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { UserAvatar } from "@/components/user-avatar"; import { VerifyForm } from "@/components/verify-form"; import { VerifyRedirectButton } from "@/components/verify-redirect-button"; -import { sendCode } from "@/lib/server/verify"; +import { sendEmailCode } from "@/lib/server/verify"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, @@ -37,26 +37,28 @@ export default async function Page(props: { searchParams: Promise }) { let human: HumanUser | undefined; let id: string | undefined; + const doSend = !skipsend && invite !== "true"; + if ("loginName" in searchParams) { sessionFactors = await loadMostRecentSession({ loginName, organization, }); - if (!skipsend && sessionFactors?.factors?.user?.id) { - await sendCode({ + if (doSend && sessionFactors?.factors?.user?.id) { + await sendEmailCode({ userId: sessionFactors?.factors?.user?.id, - isInvite: invite === "true", + authRequestId, }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Could not request email"); }); } } else if ("userId" in searchParams && userId) { - if (!skipsend) { - await sendCode({ + if (doSend) { + await sendEmailCode({ userId, - isInvite: invite === "true", + authRequestId, }).catch((error) => { console.error("Could not resend verification email", error); throw Error("Could not request email"); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 54a989f9860..b380a10a0d4 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -171,6 +171,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { session.factors?.user?.id, ); + // this can be expected to be an invite as users created in console have a password set. if (!methods.authMethodTypes || !methods.authMethodTypes.length) { const humanUser = potentialUsers[0].type.case === "human" diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index 1dab0f5c90c..bfb1ec51c05 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -7,9 +7,9 @@ import { listAuthenticationMethodTypes, resendEmailCode, resendInviteCode, - sendEmailCode, verifyEmail, verifyInviteCode, + sendEmailCode as zitadelSendEmailCode, } from "@/lib/zitadel"; import { create } from "@zitadel/client"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; @@ -192,9 +192,14 @@ export async function resendVerification(command: resendVerifyEmailCommand) { : resendEmailCode(command.userId, host, command.authRequestId); } -export async function sendCode(command: resendVerifyEmailCommand) { +type sendEmailCommand = { + userId: string; + authRequestId?: string; +}; + +export async function sendEmailCode(command: sendEmailCommand) { const host = (await headers()).get("host"); - return sendEmailCode(command.userId, host, command.authRequestId); + return zitadelSendEmailCode(command.userId, host, command.authRequestId); } export type SendVerificationRedirectWithoutCheckCommand = {