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

View File

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

View File

@@ -2,18 +2,28 @@
import { import {
createPasskeyRegistrationLink, createPasskeyRegistrationLink,
getLoginSettings,
getSession, getSession,
getUserByID,
registerPasskey, registerPasskey,
verifyPasskeyRegistration, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
} from "@/lib/zitadel"; } 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 { import {
RegisterPasskeyResponse, RegisterPasskeyResponse,
VerifyPasskeyRegistrationRequestSchema, VerifyPasskeyRegistrationRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { userAgent } from "next/server"; 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 = { type VerifyPasskeyCommand = {
passkeyId: string; passkeyId: string;
@@ -69,7 +79,7 @@ export async function registerPasskeyLink(
return registerPasskey(userId, registerLink.code, hostname); 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 // if no name is provided, try to generate one from the user agent
let passkeyName = command.passkeyName; let passkeyName = command.passkeyName;
if (!!!passkeyName) { if (!!!passkeyName) {
@@ -95,7 +105,7 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) {
throw new Error("Could not get session"); throw new Error("Could not get session");
} }
return verifyPasskeyRegistration( return zitadelVerifyPasskeyRegistration(
create(VerifyPasskeyRegistrationRequestSchema, { create(VerifyPasskeyRegistrationRequestSchema, {
passkeyId: command.passkeyId, passkeyId: command.passkeyId,
publicKeyCredential: command.publicKeyCredential, 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 { headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { checkEmailVerification, checkMFAFactors } from "../verify-helper"; import {
checkEmailVerification,
checkMFAFactors,
checkPasswordChangeRequired,
} from "../verify-helper";
type ResetPasswordCommand = { type ResetPasswordCommand = {
loginName: string; loginName: string;
@@ -138,30 +142,19 @@ export async function sendPassword(command: UpdateSessionCommand) {
const humanUser = user.type.case === "human" ? user.type.value : undefined; const humanUser = user.type.case === "human" ? user.type.value : undefined;
// check if the user has to change password first // check if the user has to change password first
if (humanUser?.passwordChangeRequired) { checkPasswordChangeRequired(
const params = new URLSearchParams({ session,
loginName: session.factors?.user?.loginName, humanUser,
}); command.organization,
command.authRequestId,
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 };
}
// throw error if user is in initial state here and do not continue // throw error if user is in initial state here and do not continue
if (user.state === UserState.INITIAL) { if (user.state === UserState.INITIAL) {
return { error: "Initial User not supported" }; return { error: "Initial User not supported" };
} }
// check to see if user was verified // check to see if user was verified
checkEmailVerification( checkEmailVerification(
session, session,
humanUser, humanUser,

View File

@@ -22,6 +22,7 @@ import {
getSessionCookieByLoginName, getSessionCookieByLoginName,
removeSessionFromCookie, removeSessionFromCookie,
} from "../cookies"; } from "../cookies";
import { checkPasswordChangeRequired } from "../verify-helper";
type CreateNewSessionCommand = { type CreateNewSessionCommand = {
userId: string; userId: string;
@@ -41,13 +42,15 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
throw new Error("No userId or loginName provided"); 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" }; 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( const session = await createSessionForIdpAndUpdateCookie(
userId, userId,
@@ -60,6 +63,22 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
return { error: "Could not create session" }; 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( const url = await getNextUrl(
authRequestId && session.id 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 { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_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( export function checkEmailVerification(
session: Session, session: Session,
humanUser?: HumanUser, humanUser?: HumanUser,