split invite, email verification

This commit is contained in:
Max Peintner
2024-12-31 14:32:24 +01:00
parent 52f26dc1ee
commit 6e7c82cc19
6 changed files with 135 additions and 88 deletions

View File

@@ -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 });
});
});

View File

@@ -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");

View File

@@ -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",

View File

@@ -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<any> }) {
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");

View File

@@ -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"

View File

@@ -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 = {