mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-24 23:18:04 +00:00
passkey actions cleanup
This commit is contained in:
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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 };
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
? {
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user