mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 15:57:32 +00:00
improve password handling
This commit is contained in:
@@ -83,6 +83,16 @@ export function RegisterPasskey({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("error" in resp && resp.error) {
|
||||||
|
setError(resp.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("passkeyId" in resp)) {
|
||||||
|
setError("An error on registering passkey");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const passkeyId = resp.passkeyId;
|
const passkeyId = resp.passkeyId;
|
||||||
const options: CredentialCreationOptions =
|
const options: CredentialCreationOptions =
|
||||||
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
||||||
@@ -92,6 +102,7 @@ export function RegisterPasskey({
|
|||||||
setError("An error on registering passkey");
|
setError("An error on registering passkey");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.publicKey.challenge = coerceToArrayBuffer(
|
options.publicKey.challenge = coerceToArrayBuffer(
|
||||||
options.publicKey.challenge,
|
options.publicKey.challenge,
|
||||||
"challenge",
|
"challenge",
|
||||||
|
@@ -5,6 +5,7 @@ import {
|
|||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
@@ -14,7 +15,8 @@ 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 crypto from "crypto";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
import { userAgent } from "next/server";
|
import { userAgent } from "next/server";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +24,7 @@ import {
|
|||||||
getSessionCookieById,
|
getSessionCookieById,
|
||||||
getSessionCookieByLoginName,
|
getSessionCookieByLoginName,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
|
import { getFingerprintId } from "../fingerprint";
|
||||||
import { getServiceUrlFromHeaders } from "../service-url";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkEmailVerification } from "../verify-helper";
|
import { checkEmailVerification } from "../verify-helper";
|
||||||
import { setSessionAndUpdateCookie } from "./cookie";
|
import { setSessionAndUpdateCookie } from "./cookie";
|
||||||
@@ -39,7 +42,7 @@ type RegisterPasskeyCommand = {
|
|||||||
|
|
||||||
export async function registerPasskeyLink(
|
export async function registerPasskeyLink(
|
||||||
command: RegisterPasskeyCommand,
|
command: RegisterPasskeyCommand,
|
||||||
): Promise<RegisterPasskeyResponse> {
|
): Promise<RegisterPasskeyResponse | { error: string }> {
|
||||||
const { sessionId } = command;
|
const { sessionId } = command;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
@@ -57,6 +60,43 @@ export async function registerPasskeyLink(
|
|||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!session?.session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not determine user from session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const authmethods = await listAuthenticationMethodTypes({
|
||||||
|
serviceUrl,
|
||||||
|
userId: session?.session?.factors?.user?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the user has no authmethods set, we need to check if the user was verified
|
||||||
|
// users are redirected from /authenticator/set to /password/set
|
||||||
|
if (authmethods.authMethodTypes.length !== 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"You have to provide a code or have a valid User Verification Check",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const cookiesList = await cookies();
|
||||||
|
const userAgentId = await getFingerprintId();
|
||||||
|
|
||||||
|
const verificationCheck = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(`${user.userId}:${userAgentId}`)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
const cookieValue = await cookiesList.get("verificationCheck")?.value;
|
||||||
|
|
||||||
|
if (!cookieValue) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookieValue !== verificationCheck) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
|
||||||
const [hostname, port] = host.split(":");
|
const [hostname, port] = host.split(":");
|
||||||
|
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
|
@@ -13,7 +13,6 @@ import {
|
|||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
listUsers,
|
listUsers,
|
||||||
passwordReset,
|
passwordReset,
|
||||||
setPassword,
|
|
||||||
setUserPassword,
|
setUserPassword,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { ConnectError, create } from "@zitadel/client";
|
import { ConnectError, create } from "@zitadel/client";
|
||||||
@@ -25,13 +24,12 @@ import {
|
|||||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import {
|
import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
AuthenticationMethodType,
|
import crypto from "crypto";
|
||||||
SetPasswordRequestSchema,
|
import { cookies, headers } from "next/headers";
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||||
|
import { getFingerprintId } from "../fingerprint";
|
||||||
import { getServiceUrlFromHeaders } from "../service-url";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import {
|
import {
|
||||||
checkEmailVerification,
|
checkEmailVerification,
|
||||||
@@ -297,6 +295,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
return { redirect: url };
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function lets users with code set a password or users with valid User Verification Check
|
||||||
export async function changePassword(command: {
|
export async function changePassword(command: {
|
||||||
code?: string;
|
code?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -316,11 +315,50 @@ export async function changePassword(command: {
|
|||||||
}
|
}
|
||||||
const userId = user.userId;
|
const userId = user.userId;
|
||||||
|
|
||||||
|
if (user.state === UserState.INITIAL) {
|
||||||
|
return { error: "User Initial State is not supported" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user has no password set in order to set a password
|
||||||
|
if (!command.code) {
|
||||||
|
const authmethods = await listAuthenticationMethodTypes({
|
||||||
|
serviceUrl,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the user has no authmethods set, we need to check if the user was verified
|
||||||
|
// users are redirected from /authenticator/set to /password/set
|
||||||
|
if (authmethods.authMethodTypes.length !== 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"You have to provide a code or have a valid User Verification Check",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const cookiesList = await cookies();
|
||||||
|
const userAgentId = await getFingerprintId();
|
||||||
|
|
||||||
|
const verificationCheck = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(`${user.userId}:${userAgentId}`)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
const cookieValue = await cookiesList.get("verificationCheck")?.value;
|
||||||
|
|
||||||
|
if (!cookieValue) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookieValue !== verificationCheck) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return setUserPassword({
|
return setUserPassword({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
password: command.password,
|
password: command.password,
|
||||||
user,
|
|
||||||
code: command.code,
|
code: command.code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -366,67 +404,32 @@ export async function checkSessionAndSetPassword({
|
|||||||
return { error: "Could not load auth methods" };
|
return { error: "Could not load auth methods" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredAuthMethodsForForceMFA = [
|
const transport = async (serviceUrl: string, token: string) => {
|
||||||
AuthenticationMethodType.OTP_EMAIL,
|
return createServerTransport(token, {
|
||||||
AuthenticationMethodType.OTP_SMS,
|
baseUrl: serviceUrl,
|
||||||
AuthenticationMethodType.TOTP,
|
|
||||||
AuthenticationMethodType.U2F,
|
|
||||||
];
|
|
||||||
|
|
||||||
const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every(
|
|
||||||
(method) => !authmethods.authMethodTypes.includes(method),
|
|
||||||
);
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization: session.factors.user.organizationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const forceMfa = !!(
|
|
||||||
loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly
|
|
||||||
);
|
|
||||||
|
|
||||||
// if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
|
|
||||||
if (forceMfa && hasNoMFAMethods) {
|
|
||||||
return setPassword({ serviceUrl, payload }).catch((error) => {
|
|
||||||
// throw error if failed precondition (ex. User is not yet initialized)
|
|
||||||
if (error.code === 9 && error.message) {
|
|
||||||
return { error: "Failed precondition" };
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
const transport = async (serviceUrl: string, token: string) => {
|
|
||||||
return createServerTransport(token, {
|
|
||||||
baseUrl: serviceUrl,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
||||||
const transportPromise = await transport(serviceUrl, sessionToken);
|
const transportPromise = await transport(serviceUrl, sessionToken);
|
||||||
return createUserServiceClient(transportPromise);
|
return createUserServiceClient(transportPromise);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selfService = await myUserService(
|
const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
|
||||||
serviceUrl,
|
|
||||||
`${sessionCookie.token}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return selfService
|
return selfService
|
||||||
.setPassword(
|
.setPassword(
|
||||||
{
|
{
|
||||||
userId: session.factors.user.id,
|
userId: session.factors.user.id,
|
||||||
newPassword: { password, changeRequired: false },
|
newPassword: { password, changeRequired: false },
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
.catch((error: ConnectError) => {
|
.catch((error: ConnectError) => {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
if (error.code === 7) {
|
if (error.code === 7) {
|
||||||
return { error: "Session is not valid." };
|
return { error: "Session is not valid." };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -29,11 +29,7 @@ import {
|
|||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQuerySchema,
|
SearchQuerySchema,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
||||||
import {
|
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
SendInviteCodeSchema,
|
|
||||||
User,
|
|
||||||
UserState,
|
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_pb";
|
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
ResendEmailCodeRequest,
|
ResendEmailCodeRequest,
|
||||||
@@ -45,10 +41,8 @@ import {
|
|||||||
VerifyPasskeyRegistrationRequest,
|
VerifyPasskeyRegistrationRequest,
|
||||||
VerifyU2FRegistrationRequest,
|
VerifyU2FRegistrationRequest,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import crypto from "crypto";
|
|
||||||
import { unstable_cacheLife as cacheLife } from "next/cache";
|
import { unstable_cacheLife as cacheLife } from "next/cache";
|
||||||
import { cookies } from "next/headers";
|
import { getUserAgent } from "./fingerprint";
|
||||||
import { getFingerprintId, getUserAgent } from "./fingerprint";
|
|
||||||
import { createServiceForHost } from "./service";
|
import { createServiceForHost } from "./service";
|
||||||
|
|
||||||
const useCache = process.env.DEBUG !== "true";
|
const useCache = process.env.DEBUG !== "true";
|
||||||
@@ -1172,13 +1166,11 @@ export async function setUserPassword({
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
password,
|
password,
|
||||||
user,
|
|
||||||
code,
|
code,
|
||||||
}: {
|
}: {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
password: string;
|
password: string;
|
||||||
user: User;
|
|
||||||
code?: string;
|
code?: string;
|
||||||
}) {
|
}) {
|
||||||
let payload = create(SetPasswordRequestSchema, {
|
let payload = create(SetPasswordRequestSchema, {
|
||||||
@@ -1188,41 +1180,6 @@ export async function setUserPassword({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// check if the user has no password set in order to set a password
|
|
||||||
if (!code) {
|
|
||||||
const authmethods = await listAuthenticationMethodTypes({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if the user has no authmethods set, we can set a password otherwise we need a code
|
|
||||||
if (
|
|
||||||
!(authmethods.authMethodTypes.length === 0) &&
|
|
||||||
user.state !== UserState.INITIAL
|
|
||||||
) {
|
|
||||||
// check if a verification was done earlier
|
|
||||||
|
|
||||||
const cookiesList = await cookies();
|
|
||||||
|
|
||||||
const userAgentId = await getFingerprintId();
|
|
||||||
|
|
||||||
const verificationCheck = crypto
|
|
||||||
.createHash("sha256")
|
|
||||||
.update(`${user.userId}:${userAgentId}`)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
await cookiesList.set({
|
|
||||||
name: "verificationCheck",
|
|
||||||
value: verificationCheck,
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
maxAge: 300, // 5 minutes
|
|
||||||
});
|
|
||||||
|
|
||||||
return { error: "Provide a code to set a password" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
payload = {
|
payload = {
|
||||||
...payload,
|
...payload,
|
||||||
|
Reference in New Issue
Block a user