Files
zitadel/apps/login/src/lib/server/passkeys.ts

211 lines
5.3 KiB
TypeScript
Raw Normal View History

2024-09-03 10:45:55 +02:00
"use server";
2024-09-03 09:35:42 +02:00
import {
createPasskeyRegistrationLink,
2024-12-20 08:44:59 +01:00
getLoginSettings,
2024-09-03 09:35:42 +02:00
getSession,
2024-12-20 08:44:59 +01:00
getUserByID,
2024-09-03 09:35:42 +02:00
registerPasskey,
2024-12-20 08:44:59 +01:00
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
2024-09-03 09:35:42 +02:00
} from "@/lib/zitadel";
2024-12-20 08:44:59 +01:00
import { create, Duration } from "@zitadel/client";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
2024-09-05 13:48:33 +02:00
import {
RegisterPasskeyResponse,
VerifyPasskeyRegistrationRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
2024-09-03 09:35:42 +02:00
import { headers } from "next/headers";
2024-09-05 13:48:33 +02:00
import { userAgent } from "next/server";
2024-12-20 08:44:59 +01:00
import { getNextUrl } from "../client";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "../cookies";
import { checkEmailVerification } from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie";
2024-09-03 09:35:42 +02:00
type VerifyPasskeyCommand = {
passkeyId: string;
passkeyName?: string;
publicKeyCredential: any;
sessionId: string;
};
type RegisterPasskeyCommand = {
sessionId: string;
};
export async function registerPasskeyLink(
command: RegisterPasskeyCommand,
): Promise<RegisterPasskeyResponse> {
const { sessionId } = command;
const sessionCookie = await getSessionCookieById({ sessionId });
2024-09-10 08:54:21 +02:00
const session = await getSession({
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
2024-09-03 09:35:42 +02:00
2024-11-22 11:25:03 +01:00
const host = (await headers()).get("host");
2024-09-03 09:35:42 +02:00
2024-10-28 16:48:57 +01:00
if (!host) {
2024-09-03 09:35:42 +02:00
throw new Error("Could not get domain");
}
2024-10-28 16:48:57 +01:00
const [hostname, port] = host.split(":");
if (!hostname) {
throw new Error("Could not get hostname");
}
2024-09-03 09:35:42 +02:00
const userId = session?.session?.factors?.user?.id;
if (!userId) {
throw new Error("Could not get session");
}
// TODO: add org context
2024-09-17 14:31:43 +02:00
// use session token to add the passkey
const registerLink = await createPasskeyRegistrationLink(
userId,
2024-09-17 14:39:43 +02:00
// sessionCookie.token,
2024-09-17 14:31:43 +02:00
);
2024-09-03 09:35:42 +02:00
if (!registerLink.code) {
throw new Error("Missing code in response");
}
2024-10-28 16:48:57 +01:00
return registerPasskey(userId, registerLink.code, hostname);
2024-09-03 09:35:42 +02:00
}
2024-12-20 08:44:59 +01:00
export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
2024-09-03 09:35:42 +02:00
// if no name is provided, try to generate one from the user agent
2024-09-05 13:38:03 +02:00
let passkeyName = command.passkeyName;
2024-09-03 09:35:42 +02:00
if (!!!passkeyName) {
2024-11-22 11:25:03 +01:00
const headersList = await headers();
2024-09-03 09:35:42 +02:00
const userAgentStructure = { headers: headersList };
const { browser, device, os } = userAgent(userAgentStructure);
passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${
device.vendor || device.model ? ", " : ""
}${os.name}${os.name ? ", " : ""}${browser.name}`;
}
2024-09-05 13:38:03 +02:00
const sessionCookie = await getSessionCookieById({
sessionId: command.sessionId,
});
2024-09-10 08:54:21 +02:00
const session = await getSession({
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
2024-09-03 09:35:42 +02:00
const userId = session?.session?.factors?.user?.id;
if (!userId) {
throw new Error("Could not get session");
}
2024-12-20 08:44:59 +01:00
return zitadelVerifyPasskeyRegistration(
2024-09-03 09:35:42 +02:00
create(VerifyPasskeyRegistrationRequestSchema, {
2024-09-05 13:38:03 +02:00
passkeyId: command.passkeyId,
publicKeyCredential: command.publicKeyCredential,
2024-09-03 09:35:42 +02:00
passkeyName,
userId,
}),
);
}
2024-12-20 08:44:59 +01:00
type SendPasskeyCommand = {
loginName?: string;
sessionId?: string;
organization?: string;
checks?: Checks;
authRequestId?: string;
lifetime?: Duration;
};
export async function sendPasskey(command: SendPasskeyCommand) {
let { loginName, sessionId, organization, checks, authRequestId } = command;
const recentSession = sessionId
? await getSessionCookieById({ sessionId })
: loginName
? await getSessionCookieByLoginName({ loginName, organization })
: await getMostRecentSessionCookie();
if (!recentSession) {
return {
error: "Could not find session",
};
}
const host = (await headers()).get("host");
if (!host) {
return { error: "Could not get host" };
}
const loginSettings = await getLoginSettings(organization);
const lifetime = checks?.webAuthN
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
: checks?.otpEmail || checks?.otpSms
? loginSettings?.secondFactorCheckLifetime
: undefined;
const session = await setSessionAndUpdateCookie(
recentSession,
checks,
undefined,
authRequestId,
lifetime,
);
if (!session || !session?.factors?.user?.id) {
return { error: "Could not update session" };
}
const userResponse = await getUserByID(session?.factors?.user?.id);
if (!userResponse.user) {
return { error: "Could not find user" };
}
const humanUser =
userResponse.user.type.case === "human"
? userResponse.user.type.value
: undefined;
2024-12-20 10:57:56 +01:00
const emailVerificationCheck = checkEmailVerification(
session,
humanUser,
organization,
authRequestId,
);
if (emailVerificationCheck?.redirect) {
return emailVerificationCheck;
}
2024-12-20 08:44:59 +01:00
const url =
authRequestId && session.id
? await getNextUrl(
{
sessionId: session.id,
authRequestId: authRequestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: session?.factors?.user?.loginName
? await getNextUrl(
{
loginName: session.factors.user.loginName,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: null;
return { redirect: url };
}