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