Merge pull request #307 from zitadel/force-mfa

additional login settings
This commit is contained in:
Max Peintner
2024-12-03 09:49:39 +01:00
committed by GitHub
14 changed files with 302 additions and 210 deletions

View File

@@ -49,6 +49,7 @@ describe("login", () => {
data: { data: {
settings: { settings: {
passkeysType: 1, passkeysType: 1,
allowUsernamePassword: true,
}, },
}, },
}); });

View File

@@ -74,6 +74,10 @@ export default async function Page(props: {
}); });
} }
if (!sessionWithData) {
return <Alert>{tError("unknownContext")}</Alert>;
}
const branding = await getBrandingSettings( const branding = await getBrandingSettings(
sessionWithData.factors?.user?.organizationId, sessionWithData.factors?.user?.organizationId,
); );
@@ -82,22 +86,32 @@ export default async function Page(props: {
sessionWithData.factors?.user?.organizationId, sessionWithData.factors?.user?.organizationId,
); );
// const identityProviders = await getActiveIdentityProviders(
// sessionWithData.factors?.user?.organizationId,
// ).then((resp) => {
// return resp.identityProviders;
// });
const params = new URLSearchParams({ const params = new URLSearchParams({
initial: "true", // defines that a code is not required and is therefore not shown in the UI initial: "true", // defines that a code is not required and is therefore not shown in the UI
}); });
if (loginName) { if (sessionWithData.factors?.user?.loginName) {
params.set("loginName", loginName); params.set("loginName", sessionWithData.factors?.user?.loginName);
} }
if (organization) { if (sessionWithData.factors?.user?.organizationId) {
params.set("organization", organization); params.set("organization", sessionWithData.factors?.user?.organizationId);
} }
if (authRequestId) { if (authRequestId) {
params.set("authRequestId", authRequestId); params.set("authRequestId", authRequestId);
} }
const host = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
@@ -105,18 +119,14 @@ export default async function Page(props: {
<p className="ztdl-p">{t("description")}</p> <p className="ztdl-p">{t("description")}</p>
{sessionWithData && (
<UserAvatar <UserAvatar
loginName={loginName ?? sessionWithData.factors?.user?.loginName} loginName={sessionWithData.factors?.user?.loginName}
displayName={sessionWithData.factors?.user?.displayName} displayName={sessionWithData.factors?.user?.displayName}
showDropdown showDropdown
searchParams={searchParams} searchParams={searchParams}
></UserAvatar> ></UserAvatar>
)}
{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>} {loginSettings && (
{loginSettings && sessionWithData && (
<ChooseAuthenticatorToSetup <ChooseAuthenticatorToSetup
authMethods={sessionWithData.authMethods} authMethods={sessionWithData.authMethods}
loginSettings={loginSettings} loginSettings={loginSettings}
@@ -124,6 +134,20 @@ export default async function Page(props: {
></ChooseAuthenticatorToSetup> ></ChooseAuthenticatorToSetup>
)} )}
{/* <p className="ztdl-p text-center">
or sign in with an Identity Provider
</p>
{loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp>
)} */}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">
<BackButton /> <BackButton />
<span className="flex-grow"></span> <span className="flex-grow"></span>

View File

@@ -37,7 +37,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" }); const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, authRequestId, organization } = searchParams; const { id, token, authRequestId, organization, link } = searchParams;
const { provider } = params; const { provider } = params;
const branding = await getBrandingSettings(organization); const branding = await getBrandingSettings(organization);
@@ -50,7 +50,8 @@ export default async function Page(props: {
const { idpInformation, userId } = intent; const { idpInformation, userId } = intent;
if (userId) { // sign in user. If user should be linked continue
if (userId && !link) {
// TODO: update user if idp.options.isAutoUpdate is true // TODO: update user if idp.options.isAutoUpdate is true
return ( return (

View File

@@ -2,23 +2,14 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UsernameForm } from "@/components/username-form"; import { UsernameForm } from "@/components/username-form";
import { import {
getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,
getLoginSettings, getLoginSettings,
settingsService,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { makeReqCtx } from "@zitadel/client/v2";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
function getIdentityProviders(orgId?: string) {
return settingsService
.getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {})
.then((resp) => {
return resp.identityProviders;
});
}
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
@@ -47,9 +38,11 @@ export default async function Page(props: {
organization ?? defaultOrganization, organization ?? defaultOrganization,
); );
const identityProviders = await getIdentityProviders( const identityProviders = await getActiveIdentityProviders(
organization ?? defaultOrganization, organization ?? defaultOrganization,
); ).then((resp) => {
return resp.identityProviders;
});
const branding = await getBrandingSettings( const branding = await getBrandingSettings(
organization ?? defaultOrganization, organization ?? defaultOrganization,
@@ -68,7 +61,7 @@ export default async function Page(props: {
submit={submit} submit={submit}
allowRegister={!!loginSettings?.allowRegister} allowRegister={!!loginSettings?.allowRegister}
> >
{identityProviders && process.env.ZITADEL_API_URL && ( {identityProviders && (
<SignInWithIdp <SignInWithIdp
host={host} host={host}
identityProviders={identityProviders} identityProviders={identityProviders}

View File

@@ -5,6 +5,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel"; import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
@@ -30,6 +31,8 @@ export default async function Page(props: {
const loginSettings = await getLoginSettings(organization); const loginSettings = await getLoginSettings(organization);
const host = (await headers()).get("host");
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
@@ -67,6 +70,8 @@ export default async function Page(props: {
organization={organization} organization={organization}
method={method} method={method}
loginSettings={loginSettings} loginSettings={loginSettings}
host={host}
code={code}
></LoginOTP> ></LoginOTP>
)} )}
</div> </div>

View File

@@ -25,6 +25,7 @@ type Props = {
method: string; method: string;
code?: string; code?: string;
loginSettings?: LoginSettings; loginSettings?: LoginSettings;
host: string | null;
}; };
type Inputs = { type Inputs = {
@@ -39,6 +40,7 @@ export function LoginOTP({
method, method,
code, code,
loginSettings, loginSettings,
host,
}: Props) { }: Props) {
const t = useTranslations("otp"); const t = useTranslations("otp");
@@ -76,7 +78,18 @@ export function LoginOTP({
if (method === "email") { if (method === "email") {
challenges = create(RequestChallengesSchema, { challenges = create(RequestChallengesSchema, {
otpEmail: { deliveryType: { case: "sendCode", value: {} } }, otpEmail: {
deliveryType: {
case: "sendCode",
value: host
? {
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
}
: {},
},
},
}); });
} }

View File

@@ -59,7 +59,6 @@ export function PasswordForm({
password: { password: values.password }, password: { password: values.password },
}), }),
authRequestId, authRequestId,
forceMfa: loginSettings?.forceMfa,
}) })
.catch(() => { .catch(() => {
setError("Could not verify password"); setError("Could not verify password");
@@ -87,6 +86,7 @@ export function PasswordForm({
const response = await resetPassword({ const response = await resetPassword({
loginName, loginName,
organization, organization,
authRequestId,
}) })
.catch(() => { .catch(() => {
setError("Could not reset password"); setError("Could not reset password");

View File

@@ -102,7 +102,7 @@ export function SessionItem({
/> />
</div> </div>
<div className="flex flex-col overflow-hidden"> <div className="flex flex-col items-start overflow-hidden">
<span className="">{session.factors?.user?.displayName}</span> <span className="">{session.factors?.user?.displayName}</span>
<span className="text-xs opacity-80 text-ellipsis"> <span className="text-xs opacity-80 text-ellipsis">
{session.factors?.user?.loginName} {session.factors?.user?.loginName}

View File

@@ -22,6 +22,7 @@ export interface SignInWithIDPProps {
identityProviders: IdentityProvider[]; identityProviders: IdentityProvider[];
authRequestId?: string; authRequestId?: string;
organization?: string; organization?: string;
linkOnly?: boolean;
} }
export function SignInWithIdp({ export function SignInWithIdp({
@@ -29,6 +30,7 @@ export function SignInWithIdp({
identityProviders, identityProviders,
authRequestId, authRequestId,
organization, organization,
linkOnly,
}: SignInWithIDPProps) { }: SignInWithIDPProps) {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@@ -39,6 +41,10 @@ export function SignInWithIdp({
const params = new URLSearchParams(); const params = new URLSearchParams();
if (linkOnly) {
params.set("link", "true");
}
if (authRequestId) { if (authRequestId) {
params.set("authRequestId", authRequestId); params.set("authRequestId", authRequestId);
} }
@@ -70,7 +76,11 @@ export function SignInWithIdp({
return ( return (
<div className="flex flex-col w-full space-y-2 text-sm"> <div className="flex flex-col w-full space-y-2 text-sm">
{identityProviders && {identityProviders &&
identityProviders.map((idp, i) => { identityProviders
// .filter((idp) =>
// linkOnly ? idp.config?.options.isLinkingAllowed : true,
// )
.map((idp, i) => {
switch (idp.type) { switch (idp.type) {
case IdentityProviderType.APPLE: case IdentityProviderType.APPLE:
return ( return (
@@ -78,7 +88,10 @@ export function SignInWithIdp({
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.APPLE)) startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.APPLE),
)
} }
></SignInWithApple> ></SignInWithApple>
); );
@@ -88,7 +101,10 @@ export function SignInWithIdp({
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OAUTH)) startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.OAUTH),
)
} }
></SignInWithGeneric> ></SignInWithGeneric>
); );
@@ -98,7 +114,10 @@ export function SignInWithIdp({
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OIDC)) startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.OIDC),
)
} }
></SignInWithGeneric> ></SignInWithGeneric>
); );

View File

@@ -61,6 +61,8 @@ export function UsernameForm({
setLoading(false); setLoading(false);
}); });
console.log(res, "res");
if (res?.redirect) { if (res?.redirect) {
return router.push(res.redirect); return router.push(res.redirect);
} }

View File

@@ -15,24 +15,29 @@ export async function getNextUrl(
defaultRedirectUri?: string, defaultRedirectUri?: string,
): Promise<string> { ): Promise<string> {
if ("sessionId" in command && "authRequestId" in command) { if ("sessionId" in command && "authRequestId" in command) {
const url = const params = new URLSearchParams({
`/login?` +
new URLSearchParams({
sessionId: command.sessionId, sessionId: command.sessionId,
authRequest: command.authRequestId, authRequest: command.authRequestId,
}); });
return url;
if (command.organization) {
params.append("organization", command.organization);
}
return `/login?` + params;
} }
if (defaultRedirectUri) { if (defaultRedirectUri) {
return defaultRedirectUri; return defaultRedirectUri;
} }
const signedInUrl = const params = new URLSearchParams({
`/signedin?` +
new URLSearchParams({
loginName: command.loginName, loginName: command.loginName,
}); });
return signedInUrl; if (command.organization) {
params.append("organization", command.organization);
}
return `/signedin?` + params;
} }

View File

@@ -2,11 +2,12 @@
import { create } from "@zitadel/client"; import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { UserState } 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";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
getIDPByID, getIDPByID,
@@ -35,6 +36,15 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const loginSettings = await getLoginSettings(command.organization); const loginSettings = await getLoginSettings(command.organization);
const potentialUsers = users.result.filter((u) => {
const human = u.type.case === "human" ? u.type.value : undefined;
return loginSettings?.disableLoginWithEmail
? human?.email?.isVerified && human?.email?.email !== command.loginName
: loginSettings?.disableLoginWithPhone
? human?.phone?.isVerified && human?.phone?.phone !== command.loginName
: true;
});
const redirectUserToSingleIDPIfAvailable = async () => { const redirectUserToSingleIDPIfAvailable = async () => {
const identityProviders = await getActiveIdentityProviders( const identityProviders = await getActiveIdentityProviders(
command.organization, command.organization,
@@ -84,6 +94,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const identityProviderId = identityProviders[0].idpId; const identityProviderId = identityProviders[0].idpId;
const idp = await getIDPByID(identityProviderId); const idp = await getIDPByID(identityProviderId);
const idpType = idp?.type; const idpType = idp?.type;
if (!idp || !idpType) { if (!idp || !idpType) {
@@ -119,8 +130,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} }
}; };
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) { if (potentialUsers.length == 1 && potentialUsers[0].userId) {
const userId = users.result[0].userId; const userId = potentialUsers[0].userId;
const checks = create(ChecksSchema, { const checks = create(ChecksSchema, {
user: { search: { case: "userId", value: userId } }, user: { search: { case: "userId", value: userId } },
@@ -136,24 +147,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
return { error: "Could not create session for user" }; return { error: "Could not create session for user" };
} }
if (users.result[0].state === UserState.INITIAL) { // TODO: check if handling of userstate INITIAL is needed
const params = new URLSearchParams({ if (potentialUsers[0].state === UserState.INITIAL) {
loginName: session.factors?.user?.loginName, return { error: "Initial User not supported" };
initial: "true", // this does not require a code to be set
});
if (command.organization || session.factors?.user?.organizationId) {
params.append(
"organization",
command.organization ?? session.factors?.user?.organizationId,
);
}
if (command.authRequestId) {
params.append("authRequestid", command.authRequestId);
}
return { redirect: "/password/set?" + params };
} }
const methods = await listAuthenticationMethodTypes( const methods = await listAuthenticationMethodTypes(
@@ -162,9 +158,9 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
if ( if (
users.result[0].type.case === "human" && potentialUsers[0].type.case === "human" &&
users.result[0].type.value.email && potentialUsers[0].type.value.email &&
!users.result[0].type.value.email.isVerified !potentialUsers[0].type.value.email.isVerified
) { ) {
const paramsVerify = new URLSearchParams({ const paramsVerify = new URLSearchParams({
loginName: session.factors?.user?.loginName, loginName: session.factors?.user?.loginName,
@@ -209,6 +205,13 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const method = methods.authMethodTypes[0]; const method = methods.authMethodTypes[0];
switch (method) { switch (method) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method case AuthenticationMethodType.PASSWORD: // user has only password as auth method
if (!loginSettings?.allowUsernamePassword) {
return {
error:
"Username Password not allowed! Contact your administrator for more information.",
};
}
const paramsPassword: any = { const paramsPassword: any = {
loginName: session.factors?.user?.loginName, loginName: session.factors?.user?.loginName,
}; };
@@ -229,6 +232,13 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}; };
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
if (loginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) {
return {
error:
"Passkeys not allowed! Contact your administrator for more information.",
};
}
const paramsPasskey: any = { loginName: command.loginName }; const paramsPasskey: any = { loginName: command.loginName };
if (command.authRequestId) { if (command.authRequestId) {
paramsPasskey.authRequestId = command.authRequestId; paramsPasskey.authRequestId = command.authRequestId;
@@ -262,7 +272,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
} else if ( } else if (
methods.authMethodTypes.includes(AuthenticationMethodType.IDP) methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
) { ) {
await redirectUserToIDP(userId); return redirectUserToIDP(userId);
} else if ( } else if (
methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)
) { ) {
@@ -288,8 +298,10 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// user not found, check if register is enabled on organization // user not found, check if register is enabled on organization
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) { if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) {
// TODO: do we need to handle login hints for IDPs here? // TODO: do we need to handle login hints for IDPs here?
await redirectUserToSingleIDPIfAvailable(); const resp = await redirectUserToSingleIDPIfAvailable();
if (resp) {
return resp;
}
return { error: "Could not find user" }; return { error: "Could not find user" };
} else if ( } else if (
loginSettings?.allowRegister && loginSettings?.allowRegister &&

View File

@@ -27,6 +27,7 @@ import { getSessionCookieByLoginName } from "../cookies";
type ResetPasswordCommand = { type ResetPasswordCommand = {
loginName: string; loginName: string;
organization?: string; organization?: string;
authRequestId?: string;
}; };
export async function resetPassword(command: ResetPasswordCommand) { export async function resetPassword(command: ResetPasswordCommand) {
@@ -46,7 +47,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
} }
const userId = users.result[0].userId; const userId = users.result[0].userId;
return passwordReset(userId, host); return passwordReset(userId, host, command.authRequestId);
} }
export type UpdateSessionCommand = { export type UpdateSessionCommand = {
@@ -54,7 +55,6 @@ export type UpdateSessionCommand = {
organization?: string; organization?: string;
checks: Checks; checks: Checks;
authRequestId?: string; authRequestId?: string;
forceMfa?: boolean;
}; };
export async function sendPassword(command: UpdateSessionCommand) { export async function sendPassword(command: UpdateSessionCommand) {
@@ -148,6 +148,27 @@ export async function sendPassword(command: UpdateSessionCommand) {
m !== AuthenticationMethodType.PASSKEY, m !== AuthenticationMethodType.PASSKEY,
); );
const humanUser = user.type.case === "human" ? user.type.value : undefined;
console.log("humanUser", humanUser);
if (
availableSecondFactors?.length == 0 &&
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 };
}
if (availableSecondFactors?.length == 1) { if (availableSecondFactors?.length == 1) {
const params = new URLSearchParams({ const params = new URLSearchParams({
loginName: session.factors?.user.loginName, loginName: session.factors?.user.loginName,
@@ -192,24 +213,14 @@ export async function sendPassword(command: UpdateSessionCommand) {
} }
return { redirect: `/mfa?` + params }; return { redirect: `/mfa?` + params };
} else if (user.state === UserState.INITIAL) {
const params = new URLSearchParams({
loginName: session.factors.user.loginName,
});
if (command.authRequestId) {
params.append("authRequestId", command.authRequestId);
} }
// TODO: check if handling of userstate INITIAL is needed
if (command.organization || session.factors?.user?.organizationId) { else if (user.state === UserState.INITIAL) {
params.append( return { error: "Initial User not supported" };
"organization", } else if (
command.organization ?? session.factors?.user?.organizationId, (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
); !availableSecondFactors.length
} ) {
return { redirect: `/password/change?` + params };
} else if (command.forceMfa && !availableSecondFactors.length) {
const params = new URLSearchParams({ const params = new URLSearchParams({
loginName: session.factors.user.loginName, loginName: session.factors.user.loginName,
force: "true", // this defines if the mfa is forced in the settings force: "true", // this defines if the mfa is forced in the settings

View File

@@ -504,7 +504,11 @@ export function createUser(
* @param userId the id of the user where the email should be set * @param userId the id of the user where the email should be set
* @returns the newly set email * @returns the newly set email
*/ */
export async function passwordReset(userId: string, host: string | null) { export async function passwordReset(
userId: string,
host: string | null,
authRequestId?: string,
) {
let medium = create(SendPasswordResetLinkSchema, { let medium = create(SendPasswordResetLinkSchema, {
notificationType: NotificationType.Email, notificationType: NotificationType.Email,
}); });
@@ -512,7 +516,9 @@ export async function passwordReset(userId: string, host: string | null) {
if (host) { if (host) {
medium = { medium = {
...medium, ...medium,
urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`, urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
}; };
} }