passkey actions cleanup

This commit is contained in:
Max Peintner
2024-12-20 08:44:59 +01:00
parent ed584c59e1
commit f1f7d661ce
6 changed files with 170 additions and 56 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { getNextUrl } from "@/lib/client";
import { sendPasskey } from "@/lib/server/passkeys";
import { updateSession } from "@/lib/server/session";
import { create, JsonObject } from "@zitadel/client";
import {
@@ -120,7 +120,7 @@ export function LoginPasskey({
async function submitLogin(data: JsonObject) {
setLoading(true);
const response = await updateSession({
const response = await sendPasskey({
loginName,
sessionId,
organization,
@@ -142,7 +142,9 @@ export function LoginPasskey({
return;
}
return response;
if (response && "redirect" in response && response.redirect) {
return router.push(response.redirect);
}
}
async function submitLoginAndContinue(
@@ -192,31 +194,7 @@ export function LoginPasskey({
},
};
return submitLogin(data).then(async (resp) => {
const url =
authRequestId && resp?.sessionId
? await getNextUrl(
{
sessionId: resp.sessionId,
authRequestId: authRequestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: resp?.factors?.user?.loginName
? await getNextUrl(
{
loginName: resp.factors.user.loginName,
organization: organization,
},
loginSettings?.defaultRedirectUri,
)
: null;
if (url) {
router.push(url);
}
});
return submitLogin(data);
})
.finally(() => {
setLoading(false);

View File

@@ -1,7 +1,10 @@
"use client";
import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64";
import { registerPasskeyLink, verifyPasskey } from "@/lib/server/passkeys";
import {
registerPasskeyLink,
verifyPasskeyRegistration,
} from "@/lib/server/passkeys";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
@@ -45,7 +48,7 @@ export function RegisterPasskey({
sessionId: string,
) {
setLoading(true);
const response = await verifyPasskey({
const response = await verifyPasskeyRegistration({
passkeyId,
passkeyName,
publicKeyCredential,

View File

@@ -2,18 +2,28 @@
import {
createPasskeyRegistrationLink,
getLoginSettings,
getSession,
getUserByID,
registerPasskey,
verifyPasskeyRegistration,
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
} from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { create, Duration } from "@zitadel/client";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import {
RegisterPasskeyResponse,
VerifyPasskeyRegistrationRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { userAgent } from "next/server";
import { getSessionCookieById } from "../cookies";
import { getNextUrl } from "../client";
import {
getMostRecentSessionCookie,
getSessionCookieById,
getSessionCookieByLoginName,
} from "../cookies";
import { checkEmailVerification } from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie";
type VerifyPasskeyCommand = {
passkeyId: string;
@@ -69,7 +79,7 @@ export async function registerPasskeyLink(
return registerPasskey(userId, registerLink.code, hostname);
}
export async function verifyPasskey(command: VerifyPasskeyCommand) {
export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
// if no name is provided, try to generate one from the user agent
let passkeyName = command.passkeyName;
if (!!!passkeyName) {
@@ -95,7 +105,7 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) {
throw new Error("Could not get session");
}
return verifyPasskeyRegistration(
return zitadelVerifyPasskeyRegistration(
create(VerifyPasskeyRegistrationRequestSchema, {
passkeyId: command.passkeyId,
publicKeyCredential: command.publicKeyCredential,
@@ -104,3 +114,88 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) {
}),
);
}
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;
checkEmailVerification(session, humanUser, organization, authRequestId);
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 };
}

View File

@@ -30,7 +30,11 @@ import {
import { headers } from "next/headers";
import { getNextUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
import {
checkEmailVerification,
checkMFAFactors,
checkPasswordChangeRequired,
} from "../verify-helper";
type ResetPasswordCommand = {
loginName: string;
@@ -138,30 +142,19 @@ export async function sendPassword(command: UpdateSessionCommand) {
const humanUser = user.type.case === "human" ? user.type.value : undefined;
// check if the user has to change password first
if (humanUser?.passwordChangeRequired) {
const params = new URLSearchParams({
loginName: session.factors?.user?.loginName,
});
if (command.organization || session.factors?.user?.organizationId) {
params.append("organization", session.factors?.user?.organizationId);
}
if (command.authRequestId) {
params.append("authRequestId", command.authRequestId);
}
return { redirect: "/password/change?" + params };
}
checkPasswordChangeRequired(
session,
humanUser,
command.organization,
command.authRequestId,
);
// throw error if user is in initial state here and do not continue
if (user.state === UserState.INITIAL) {
return { error: "Initial User not supported" };
}
// check to see if user was verified
checkEmailVerification(
session,
humanUser,

View File

@@ -22,6 +22,7 @@ import {
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "../cookies";
import { checkPasswordChangeRequired } from "../verify-helper";
type CreateNewSessionCommand = {
userId: string;
@@ -41,13 +42,15 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
throw new Error("No userId or loginName provided");
}
const user = await getUserByID(userId);
const userResponse = await getUserByID(userId);
if (!user) {
if (!userResponse || !userResponse.user) {
return { error: "Could not find user" };
}
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
const loginSettings = await getLoginSettings(
userResponse.user.details?.resourceOwner,
);
const session = await createSessionForIdpAndUpdateCookie(
userId,
@@ -60,6 +63,22 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
return { error: "Could not create session" };
}
const humanUser =
userResponse.user.type.case === "human"
? userResponse.user.type.value
: undefined;
// check if the user has to change password first
checkPasswordChangeRequired(
session,
humanUser,
session.factors.user.organizationId,
authRequestId,
);
// TODO: check if user has MFA methods
// checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId);
const url = await getNextUrl(
authRequestId && session.id
? {

View File

@@ -3,6 +3,32 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
export function checkPasswordChangeRequired(
session: Session,
humanUser: HumanUser | undefined,
organization?: string,
authRequestId?: string,
) {
if (humanUser?.passwordChangeRequired) {
const params = new URLSearchParams({
loginName: session.factors?.user?.loginName as string,
});
if (organization || session.factors?.user?.organizationId) {
params.append(
"organization",
session.factors?.user?.organizationId as string,
);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
}
return { redirect: "/password/change?" + params };
}
}
export function checkEmailVerification(
session: Session,
humanUser?: HumanUser,