mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 22:22:31 +00:00
11
README.md
11
README.md
@@ -115,14 +115,17 @@ You can already use the current state, and extend it with your needs.
|
|||||||
passkey --> B[signedin]
|
passkey --> B[signedin]
|
||||||
password -- hasMFA --> mfa
|
password -- hasMFA --> mfa
|
||||||
password -- allowPasskeys --> passkey-add
|
password -- allowPasskeys --> passkey-add
|
||||||
|
password -- reset --> password-set
|
||||||
|
email -- reset --> password-set
|
||||||
|
password-set --> B[signedin]
|
||||||
|
password-change --> B[signedin]
|
||||||
|
password -- userstate=initial --> password-change
|
||||||
|
|
||||||
mfa --> otp
|
mfa --> otp
|
||||||
otp --> B[signedin]
|
otp --> B[signedin]
|
||||||
mfa--> u2f
|
mfa--> u2f
|
||||||
u2f -->B[signedin]
|
u2f -->B[signedin]
|
||||||
register --> passkey-add
|
register -- password/passkey --> B[signedin]
|
||||||
register --> password-set
|
|
||||||
password-set --> B[signedin]
|
|
||||||
passkey-add --> B[signedin]
|
|
||||||
password --> B[signedin]
|
password --> B[signedin]
|
||||||
password-- forceMFA -->mfaset
|
password-- forceMFA -->mfaset
|
||||||
mfaset --> u2fset
|
mfaset --> u2fset
|
||||||
|
|||||||
@@ -104,16 +104,16 @@ describe("login", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => {
|
// it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => {
|
||||||
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
|
// cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
|
||||||
cy.location("pathname", { timeout: 10_000 }).should("eq", "/password");
|
// cy.location("pathname", { timeout: 10_000 }).should("eq", "/password");
|
||||||
cy.get('input[type="password"]').focus().type("MyStrongPassword!1");
|
// cy.get('input[type="password"]').focus().type("MyStrongPassword!1");
|
||||||
cy.get('button[type="submit"]').click();
|
// cy.get('button[type="submit"]').click();
|
||||||
cy.location("pathname", { timeout: 10_000 }).should(
|
// cy.location("pathname", { timeout: 10_000 }).should(
|
||||||
"eq",
|
// "eq",
|
||||||
"/passkey/set",
|
// "/passkey/set",
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("passkey login", () => {
|
describe("passkey login", () => {
|
||||||
|
|||||||
@@ -14,10 +14,24 @@
|
|||||||
"register": "Neuen Benutzer registrieren"
|
"register": "Neuen Benutzer registrieren"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Passwort",
|
"verify": {
|
||||||
"description": "Geben Sie Ihr Passwort ein.",
|
"title": "Passwort",
|
||||||
"resetPassword": "Passwort zurücksetzen",
|
"description": "Geben Sie Ihr Passwort ein.",
|
||||||
"submit": "Weiter"
|
"resetPassword": "Passwort zurücksetzen",
|
||||||
|
"submit": "Weiter"
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"title": "Passwort festlegen",
|
||||||
|
"description": "Legen Sie das Passwort für Ihr Konto fest",
|
||||||
|
"codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.",
|
||||||
|
"resend": "Erneut senden",
|
||||||
|
"submit": "Weiter"
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"title": "Passwort ändern",
|
||||||
|
"description": "Legen Sie das Passwort für Ihr Konto fest",
|
||||||
|
"submit": "Weiter"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"idp": {
|
"idp": {
|
||||||
"title": "Mit SSO anmelden",
|
"title": "Mit SSO anmelden",
|
||||||
@@ -134,6 +148,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",
|
"unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",
|
||||||
"sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an."
|
"sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||||
|
"failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,24 @@
|
|||||||
"register": "Register new user"
|
"register": "Register new user"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Password",
|
"verify": {
|
||||||
"description": "Enter your password.",
|
"title": "Password",
|
||||||
"resetPassword": "Reset Password",
|
"description": "Enter your password.",
|
||||||
"submit": "Continue"
|
"resetPassword": "Reset Password",
|
||||||
|
"submit": "Continue"
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"title": "Set Password",
|
||||||
|
"description": "Set the password for your account",
|
||||||
|
"codeSent": "A code has been sent to your email address.",
|
||||||
|
"resend": "Resend code",
|
||||||
|
"submit": "Continue"
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"title": "Change Password",
|
||||||
|
"description": "Set the password for your account",
|
||||||
|
"submit": "Continue"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"idp": {
|
"idp": {
|
||||||
"title": "Sign in with SSO",
|
"title": "Sign in with SSO",
|
||||||
@@ -134,6 +148,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",
|
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",
|
||||||
"sessionExpired": "Your current session has expired. Please login again."
|
"sessionExpired": "Your current session has expired. Please login again.",
|
||||||
|
"failedLoading": "Failed to load data. Please try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,24 @@
|
|||||||
"register": "Registrar nuevo usuario"
|
"register": "Registrar nuevo usuario"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Contraseña",
|
"verify": {
|
||||||
"description": "Introduce tu contraseña.",
|
"title": "Contraseña",
|
||||||
"resetPassword": "Restablecer Contraseña",
|
"description": "Introduce tu contraseña.",
|
||||||
"submit": "Continuar"
|
"resetPassword": "Restablecer contraseña",
|
||||||
|
"submit": "Continuar"
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"title": "Establecer Contraseña",
|
||||||
|
"description": "Establece la contraseña para tu cuenta",
|
||||||
|
"codeSent": "Se ha enviado un código a su correo electrónico.",
|
||||||
|
"resend": "Reenviar código",
|
||||||
|
"submit": "Continuar"
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"title": "Cambiar Contraseña",
|
||||||
|
"description": "Establece la contraseña para tu cuenta",
|
||||||
|
"submit": "Continuar"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"idp": {
|
"idp": {
|
||||||
"title": "Iniciar sesión con SSO",
|
"title": "Iniciar sesión con SSO",
|
||||||
@@ -134,6 +148,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",
|
"unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",
|
||||||
"sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo."
|
"sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.",
|
||||||
|
"failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,24 @@
|
|||||||
"register": "Registrati come nuovo utente"
|
"register": "Registrati come nuovo utente"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"title": "Password",
|
"verify": {
|
||||||
"description": "Inserisci la tua password.",
|
"title": "Password",
|
||||||
"resetPassword": "Reimposta Password",
|
"description": "Inserisci la tua password.",
|
||||||
"submit": "Continua"
|
"resetPassword": "Reimposta Password",
|
||||||
|
"submit": "Continua"
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
"title": "Imposta Password",
|
||||||
|
"description": "Imposta la password per il tuo account",
|
||||||
|
"codeSent": "Un codice è stato inviato al tuo indirizzo email.",
|
||||||
|
"resend": "Invia di nuovo",
|
||||||
|
"submit": "Continua"
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"title": "Cambia Password",
|
||||||
|
"description": "Imposta la password per il tuo account",
|
||||||
|
"submit": "Continua"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"idp": {
|
"idp": {
|
||||||
"title": "Accedi con SSO",
|
"title": "Accedi con SSO",
|
||||||
@@ -134,6 +148,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",
|
"unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",
|
||||||
"sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso."
|
"sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.",
|
||||||
|
"failedLoading": "Impossibile caricare i dati. Riprova."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,14 +24,17 @@ This diagram shows the available pages and flows.
|
|||||||
passkey --> B[signedin]
|
passkey --> B[signedin]
|
||||||
password -- hasMFA --> mfa
|
password -- hasMFA --> mfa
|
||||||
password -- allowPasskeys --> passkey-add
|
password -- allowPasskeys --> passkey-add
|
||||||
|
password -- reset --> password-set
|
||||||
|
email -- reset --> password-set
|
||||||
|
password-set --> B[signedin]
|
||||||
|
password-change --> B[signedin]
|
||||||
|
password -- userstate=initial --> password-change
|
||||||
|
|
||||||
mfa --> otp
|
mfa --> otp
|
||||||
otp --> B[signedin]
|
otp --> B[signedin]
|
||||||
mfa--> u2f
|
mfa--> u2f
|
||||||
u2f -->B[signedin]
|
u2f -->B[signedin]
|
||||||
register --> passkey-add
|
register -- password/passkey --> B[signedin]
|
||||||
register --> password-set
|
|
||||||
password-set --> B[signedin]
|
|
||||||
passkey-add --> B[signedin]
|
|
||||||
password --> B[signedin]
|
password --> B[signedin]
|
||||||
password-- forceMFA -->mfaset
|
password-- forceMFA -->mfaset
|
||||||
mfaset --> u2fset
|
mfaset --> u2fset
|
||||||
@@ -103,10 +106,14 @@ Requests to the APIs made:
|
|||||||
- `listAuthenticationMethodTypes`
|
- `listAuthenticationMethodTypes`
|
||||||
- `getSession()`
|
- `getSession()`
|
||||||
- `updateSession()`
|
- `updateSession()`
|
||||||
|
- `listUsers()`
|
||||||
|
- `getUserById()`
|
||||||
|
|
||||||
**MFA AVAILABLE:** After the password has been submitted, additional authentication methods are loaded.
|
**MFA AVAILABLE:** After the password has been submitted, additional authentication methods are loaded.
|
||||||
If the user has set up an additional **single** second factor, it is redirected to add the next factor. Depending on the available method he is redirected to `/otp/time-based`,`/otp/sms?`, `/otp/email?` or `/u2f?`. If the user has multiple second factors, he is redirected to `/mfa` to select his preferred method to continue.
|
If the user has set up an additional **single** second factor, it is redirected to add the next factor. Depending on the available method he is redirected to `/otp/time-based`,`/otp/sms?`, `/otp/email?` or `/u2f?`. If the user has multiple second factors, he is redirected to `/mfa` to select his preferred method to continue.
|
||||||
|
|
||||||
|
**NO MFA, USER STATE INITIAL** If the user has no MFA methods and is in an initial state, we redirect to `/password/change` where a new password can be set.
|
||||||
|
|
||||||
**NO MFA, FORCE MFA:** If no MFA method is available, and the settings force MFA, the user is sent to `/mfa/set` which prompts to setup a second factor.
|
**NO MFA, FORCE MFA:** If no MFA method is available, and the settings force MFA, the user is sent to `/mfa/set` which prompts to setup a second factor.
|
||||||
|
|
||||||
**PROMPT PASSKEY** If the settings do not enforce MFA, we check if passkeys are allowed with `loginSettings?.passkeysType === PasskeysType.ALLOWED` and redirect the user to `/passkey/set` if no passkeys are setup. This step can be skipped.
|
**PROMPT PASSKEY** If the settings do not enforce MFA, we check if passkeys are allowed with `loginSettings?.passkeysType === PasskeysType.ALLOWED` and redirect the user to `/passkey/set` if no passkeys are setup. This step can be skipped.
|
||||||
@@ -115,6 +122,38 @@ If none of the previous conditions apply, we continue to sign in.
|
|||||||
|
|
||||||
> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f methods or passkeys. The check whether a user should be redirected to one of the pages `/passkey` or `/u2f`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615)
|
> NOTE: `listAuthenticationMethodTypes()` does not consider different domains for u2f methods or passkeys. The check whether a user should be redirected to one of the pages `/passkey` or `/u2f`, should be extended to use a domain filter (https://github.com/zitadel/zitadel/issues/8615)
|
||||||
|
|
||||||
|
### /password/change
|
||||||
|
|
||||||
|
This page allows to change the password. It is used after a user is in an initial state and is required to change the password, or it can be directly invoked with an active session.
|
||||||
|
|
||||||
|
<img src="./screenshots/password_change.png" alt="/password/change" width="400px" />
|
||||||
|
|
||||||
|
Requests to the APIs made:
|
||||||
|
|
||||||
|
- `getLoginSettings(org?)`
|
||||||
|
- `getPasswordComplexitySettings(user?)`
|
||||||
|
- `getBrandingSettings(org?)`
|
||||||
|
- `getSession()`
|
||||||
|
- `setPassword()`
|
||||||
|
|
||||||
|
> NOTE: The request to change the password is using the session of the user itself not the service user, therefore no code is required.
|
||||||
|
|
||||||
|
### /password/set
|
||||||
|
|
||||||
|
This page allows to set a password. It is used after a user has requested to reset the password on the `/password` page.
|
||||||
|
|
||||||
|
<img src="./screenshots/password_set.png" alt="/password/set" width="400px" />
|
||||||
|
|
||||||
|
Requests to the APIs made:
|
||||||
|
|
||||||
|
- `getLoginSettings(org?)`
|
||||||
|
- `getPasswordComplexitySettings(user?)`
|
||||||
|
- `getBrandingSettings(org?)`
|
||||||
|
- `getUserByID()`
|
||||||
|
- `setPassword()`
|
||||||
|
|
||||||
|
The page allows to enter a code or be invoked directly from a email link which prefills the code. The user can enter a new password and submit.
|
||||||
|
|
||||||
### /otp/[method]
|
### /otp/[method]
|
||||||
|
|
||||||
This page shows a code field to check an otp method. The session of the user is then hydrated with the respective factor. Supported methods are `time-based`, `sms` and `email`.
|
This page shows a code field to check an otp method. The session of the user is then hydrated with the respective factor. Supported methods are `time-based`, `sms` and `email`.
|
||||||
|
|||||||
BIN
apps/login/screenshots/password_change.png
Normal file
BIN
apps/login/screenshots/password_change.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
apps/login/screenshots/password_set.png
Normal file
BIN
apps/login/screenshots/password_set.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
@@ -1,78 +0,0 @@
|
|||||||
import { Alert } from "@/components/alert";
|
|
||||||
import { ChangePasswordForm } from "@/components/change-password-form";
|
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
|
||||||
import {
|
|
||||||
getBrandingSettings,
|
|
||||||
getPasswordComplexitySettings,
|
|
||||||
getSession,
|
|
||||||
} from "@/lib/zitadel";
|
|
||||||
|
|
||||||
export default async function Page({
|
|
||||||
searchParams,
|
|
||||||
}: {
|
|
||||||
searchParams: Record<string | number | symbol, string | undefined>;
|
|
||||||
}) {
|
|
||||||
const { sessionId } = searchParams;
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Session ID not found</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionCookie = await getSessionCookieById({
|
|
||||||
sessionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { session } = await getSession({
|
|
||||||
sessionId: sessionCookie.id,
|
|
||||||
sessionToken: sessionCookie.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const passwordComplexitySettings = await getPasswordComplexitySettings(
|
|
||||||
session?.factors?.user?.organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings(
|
|
||||||
session?.factors?.user?.organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DynamicTheme branding={branding}>
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>Set Password</h1>
|
|
||||||
<p className="ztdl-p">Set the password for your account</p>
|
|
||||||
|
|
||||||
{!session && (
|
|
||||||
<div className="py-4">
|
|
||||||
<Alert>
|
|
||||||
Could not get the context of the user. Make sure to enter the
|
|
||||||
username first or provide a loginName as searchParam.
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{session && (
|
|
||||||
<UserAvatar
|
|
||||||
loginName={session.factors?.user?.loginName}
|
|
||||||
displayName={session.factors?.user?.displayName}
|
|
||||||
showDropdown
|
|
||||||
searchParams={searchParams}
|
|
||||||
></UserAvatar>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{passwordComplexitySettings && session?.factors?.user?.id && (
|
|
||||||
<ChangePasswordForm
|
|
||||||
passwordComplexitySettings={passwordComplexitySettings}
|
|
||||||
userId={session.factors.user.id}
|
|
||||||
sessionId={sessionId}
|
|
||||||
></ChangePasswordForm>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DynamicTheme>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
80
apps/login/src/app/(login)/password/change/page.tsx
Normal file
80
apps/login/src/app/(login)/password/change/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Alert } from "@/components/alert";
|
||||||
|
import { ChangePasswordForm } from "@/components/change-password-form";
|
||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getPasswordComplexitySettings,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "password" });
|
||||||
|
|
||||||
|
const { loginName, organization, authRequestId, code } = searchParams;
|
||||||
|
|
||||||
|
// also allow no session to be found (ignoreUnkownUsername)
|
||||||
|
const sessionFactors = await loadMostRecentSession({
|
||||||
|
loginName,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
const passwordComplexity = await getPasswordComplexitySettings(
|
||||||
|
sessionFactors?.factors?.user?.organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>
|
||||||
|
{sessionFactors?.factors?.user?.displayName ?? t("change.title")}
|
||||||
|
</h1>
|
||||||
|
<p className="ztdl-p mb-6 block">{t("change.description")}</p>
|
||||||
|
|
||||||
|
{/* show error only if usernames should be shown to be unknown */}
|
||||||
|
{(!sessionFactors || !loginName) &&
|
||||||
|
!loginSettings?.ignoreUnknownUsernames && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{t("error:unknownContext")}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessionFactors && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{passwordComplexity &&
|
||||||
|
loginName &&
|
||||||
|
sessionFactors?.factors?.user?.id ? (
|
||||||
|
<ChangePasswordForm
|
||||||
|
sessionId={sessionFactors.id}
|
||||||
|
loginName={loginName}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
passwordComplexitySettings={passwordComplexity}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{t("error:failedLoading")}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,8 +35,10 @@ export default async function Page({
|
|||||||
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">
|
||||||
<h1>{sessionFactors?.factors?.user?.displayName ?? t("title")}</h1>
|
<h1>
|
||||||
<p className="ztdl-p mb-6 block">{t("description")}</p>
|
{sessionFactors?.factors?.user?.displayName ?? t("verify.title")}
|
||||||
|
</h1>
|
||||||
|
<p className="ztdl-p mb-6 block">{t("verify.description")}</p>
|
||||||
|
|
||||||
{/* show error only if usernames should be shown to be unknown */}
|
{/* show error only if usernames should be shown to be unknown */}
|
||||||
{(!sessionFactors || !loginName) &&
|
{(!sessionFactors || !loginName) &&
|
||||||
|
|||||||
81
apps/login/src/app/(login)/password/set/page.tsx
Normal file
81
apps/login/src/app/(login)/password/set/page.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { SetPasswordForm } from "@/components/set-password-form";
|
||||||
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getPasswordComplexitySettings,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Record<string | number | symbol, string | undefined>;
|
||||||
|
}) {
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "password" });
|
||||||
|
|
||||||
|
const { loginName, organization, authRequestId, code } = searchParams;
|
||||||
|
|
||||||
|
// also allow no session to be found (ignoreUnkownUsername)
|
||||||
|
const sessionFactors = await loadMostRecentSession({
|
||||||
|
loginName,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings(organization);
|
||||||
|
|
||||||
|
const passwordComplexity = await getPasswordComplexitySettings(
|
||||||
|
sessionFactors?.factors?.user?.organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings(organization);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{sessionFactors?.factors?.user?.displayName ?? t("set.title")}</h1>
|
||||||
|
<p className="ztdl-p mb-6 block">{t("set.description")}</p>
|
||||||
|
|
||||||
|
{/* show error only if usernames should be shown to be unknown */}
|
||||||
|
{(!sessionFactors || !loginName) &&
|
||||||
|
!loginSettings?.ignoreUnknownUsernames && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{t("error:unknownContext")}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessionFactors && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert type={AlertType.INFO}>{t("set.codeSent")}</Alert>
|
||||||
|
|
||||||
|
{passwordComplexity &&
|
||||||
|
loginName &&
|
||||||
|
sessionFactors?.factors?.user?.id ? (
|
||||||
|
<SetPasswordForm
|
||||||
|
code={code}
|
||||||
|
userId={sessionFactors.factors.user.id}
|
||||||
|
loginName={loginName}
|
||||||
|
authRequestId={authRequestId}
|
||||||
|
organization={organization}
|
||||||
|
passwordComplexitySettings={passwordComplexity}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{t("error:failedLoading")}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { RegisterFormWithoutPassword } from "@/components/register-form-without-password";
|
import { RegisterFormWithoutPassword } from "@/components/register-form-without-password";
|
||||||
import { SetPasswordForm } from "@/components/set-password-form";
|
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getLegalAndSupportSettings,
|
getLegalAndSupportSettings,
|
||||||
@@ -38,14 +38,14 @@ export default async function Page({
|
|||||||
<p className="ztdl-p">{t("description")}</p>
|
<p className="ztdl-p">{t("description")}</p>
|
||||||
|
|
||||||
{legal && passwordComplexitySettings && (
|
{legal && passwordComplexitySettings && (
|
||||||
<SetPasswordForm
|
<SetRegisterPasswordForm
|
||||||
passwordComplexitySettings={passwordComplexitySettings}
|
passwordComplexitySettings={passwordComplexitySettings}
|
||||||
email={email}
|
email={email}
|
||||||
firstname={firstname}
|
firstname={firstname}
|
||||||
lastname={lastname}
|
lastname={lastname}
|
||||||
organization={organization}
|
organization={organization}
|
||||||
authRequestId={authRequestId}
|
authRequestId={authRequestId}
|
||||||
></SetPasswordForm>
|
></SetRegisterPasswordForm>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import {
|
|||||||
symbolValidator,
|
symbolValidator,
|
||||||
upperCaseValidator,
|
upperCaseValidator,
|
||||||
} from "@/helpers/validators";
|
} from "@/helpers/validators";
|
||||||
import { setPassword } from "@/lib/self";
|
import { setMyPassword } from "@/lib/self";
|
||||||
|
import { sendPassword } from "@/lib/server/password";
|
||||||
|
import { create } from "@zitadel/client";
|
||||||
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FieldValues, useForm } from "react-hook-form";
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
@@ -27,15 +31,21 @@ type Inputs =
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
passwordComplexitySettings: PasswordComplexitySettings;
|
passwordComplexitySettings: PasswordComplexitySettings;
|
||||||
userId: string;
|
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
loginName: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
organization?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ChangePasswordForm({
|
export function ChangePasswordForm({
|
||||||
passwordComplexitySettings,
|
passwordComplexitySettings,
|
||||||
userId,
|
|
||||||
sessionId,
|
sessionId,
|
||||||
|
loginName,
|
||||||
|
authRequestId,
|
||||||
|
organization,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const t = useTranslations("password");
|
||||||
|
|
||||||
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
|
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -51,9 +61,8 @@ export function ChangePasswordForm({
|
|||||||
|
|
||||||
async function submitChange(values: Inputs) {
|
async function submitChange(values: Inputs) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await setPassword({
|
const changeResponse = await setMyPassword({
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
userId: userId,
|
|
||||||
password: values.password,
|
password: values.password,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setError("Could not change password");
|
setError("Could not change password");
|
||||||
@@ -61,12 +70,40 @@ export function ChangePasswordForm({
|
|||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!response) {
|
if (changeResponse && "error" in changeResponse) {
|
||||||
|
setError(changeResponse.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changeResponse) {
|
||||||
setError("Could not change password");
|
setError("Could not change password");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
const passwordResponse = await sendPassword({
|
||||||
|
loginName,
|
||||||
|
organization,
|
||||||
|
checks: create(ChecksSchema, {
|
||||||
|
password: { password: values.password },
|
||||||
|
}),
|
||||||
|
authRequestId,
|
||||||
|
}).catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setError("Could not verify password");
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (
|
||||||
|
passwordResponse &&
|
||||||
|
"error" in passwordResponse &&
|
||||||
|
passwordResponse.error
|
||||||
|
) {
|
||||||
|
setError(passwordResponse.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
@@ -99,9 +136,9 @@ export function ChangePasswordForm({
|
|||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
{...register("password", {
|
{...register("password", {
|
||||||
required: "You have to provide a password!",
|
required: "You have to provide a new password!",
|
||||||
})}
|
})}
|
||||||
label="Password"
|
label="New Password"
|
||||||
error={errors.password?.message as string}
|
error={errors.password?.message as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +180,7 @@ export function ChangePasswordForm({
|
|||||||
onClick={handleSubmit(submitChange)}
|
onClick={handleSubmit(submitChange)}
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
continue
|
{t("change.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export function LoginOTP({
|
|||||||
<Alert type={AlertType.INFO}>
|
<Alert type={AlertType.INFO}>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<span className="flex-1 mr-auto text-left">
|
<span className="flex-1 mr-auto text-left">
|
||||||
{t("noCodeReceived")}
|
{t("verify.noCodeReceived")}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
aria-label="Resend OTP Code"
|
aria-label="Resend OTP Code"
|
||||||
@@ -212,7 +212,7 @@ export function LoginOTP({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("resendCode")}
|
{t("verify.resendCode")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -244,7 +244,7 @@ export function LoginOTP({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
{t("submit")}
|
{t("verify.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { resetPassword, sendPassword } from "@/lib/server/password";
|
|||||||
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 { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
|
||||||
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";
|
||||||
@@ -60,16 +59,19 @@ export function PasswordForm({
|
|||||||
password: { password: values.password },
|
password: { password: values.password },
|
||||||
}),
|
}),
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
forceMfa: loginSettings?.forceMfa,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
setLoading(false);
|
||||||
setError("Could not verify password");
|
setError("Could not verify password");
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
if (response && "error" in response && response.error) {
|
if (response && "error" in response && response.error) {
|
||||||
setError(response.error);
|
setError(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,146 +87,29 @@ export function PasswordForm({
|
|||||||
setError("Could not reset password");
|
setError("Could not reset password");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && "error" in response) {
|
|
||||||
setError(response.error);
|
|
||||||
} else {
|
|
||||||
setInfo("Password was reset. Please check your email.");
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
return response;
|
if (response && "error" in response) {
|
||||||
}
|
setError(response.error);
|
||||||
|
|
||||||
async function submitPasswordAndContinue(
|
|
||||||
value: Inputs,
|
|
||||||
): Promise<boolean | void> {
|
|
||||||
const submitted = await submitPassword(value);
|
|
||||||
setInfo("");
|
|
||||||
// if user has mfa -> /otp/[method] or /u2f
|
|
||||||
// if mfa is forced and user has no mfa -> /mfa/set
|
|
||||||
// if no passwordless -> /passkey/set
|
|
||||||
|
|
||||||
// exclude password and passwordless
|
|
||||||
if (
|
|
||||||
!submitted ||
|
|
||||||
!submitted.authMethods ||
|
|
||||||
!submitted.factors?.user?.loginName
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableSecondFactors = submitted?.authMethods?.filter(
|
setInfo("Password was reset. Please check your email.");
|
||||||
(m: AuthenticationMethodType) =>
|
|
||||||
m !== AuthenticationMethodType.PASSWORD &&
|
|
||||||
m !== AuthenticationMethodType.PASSKEY,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (availableSecondFactors?.length == 1) {
|
const params = new URLSearchParams({
|
||||||
const params = new URLSearchParams({
|
loginName: loginName,
|
||||||
loginName: submitted.factors?.user.loginName,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
params.append("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
const factor = availableSecondFactors[0];
|
|
||||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
|
||||||
if (factor === AuthenticationMethodType.TOTP) {
|
|
||||||
return router.push(`/otp/time-based?` + params);
|
|
||||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
|
||||||
return router.push(`/otp/sms?` + params);
|
|
||||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
|
||||||
return router.push(`/otp/email?` + params);
|
|
||||||
} else if (factor === AuthenticationMethodType.U2F) {
|
|
||||||
return router.push(`/u2f?` + params);
|
|
||||||
}
|
|
||||||
} else if (availableSecondFactors?.length >= 1) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: submitted.factors.user.loginName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
params.append("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/mfa?` + params);
|
|
||||||
} else if (loginSettings?.forceMfa && !availableSecondFactors.length) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: submitted.factors.user.loginName,
|
|
||||||
force: "true", // this defines if the mfa is forced in the settings
|
|
||||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
params.append("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: provide a way to setup passkeys on mfa page?
|
|
||||||
return router.push(`/mfa/set?` + params);
|
|
||||||
} else if (
|
|
||||||
submitted.factors &&
|
|
||||||
!submitted.factors.webAuthN && // if session was not verified with a passkey
|
|
||||||
promptPasswordless && // if explicitly prompted due policy
|
|
||||||
!isAlternative // escaped if password was used as an alternative method
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
loginName: submitted.factors.user.loginName,
|
|
||||||
prompt: "true",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
params.append("authRequestId", authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
params.append("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/passkey/set?` + params);
|
|
||||||
} else if (authRequestId && submitted.sessionId) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
sessionId: submitted.sessionId,
|
|
||||||
authRequest: authRequestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
params.append("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(`/login?` + params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// without OIDC flow
|
|
||||||
const params = new URLSearchParams(
|
|
||||||
authRequestId
|
|
||||||
? {
|
|
||||||
loginName: submitted.factors.user.loginName,
|
|
||||||
authRequestId,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
loginName: submitted.factors.user.loginName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
params.append("organization", organization);
|
params.append("organization", organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return router.push(`/signedin?` + params);
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push("/password/set?" + params);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -243,7 +128,7 @@ export function PasswordForm({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{t("resetPassword")}
|
{t("verify.resetPassword")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -277,10 +162,10 @@ export function PasswordForm({
|
|||||||
className="self-end"
|
className="self-end"
|
||||||
variant={ButtonVariants.Primary}
|
variant={ButtonVariants.Primary}
|
||||||
disabled={loading || !formState.isValid}
|
disabled={loading || !formState.isValid}
|
||||||
onClick={handleSubmit(submitPasswordAndContinue)}
|
onClick={handleSubmit(submitPassword)}
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
{t("submit")}
|
{t("verify.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
symbolValidator,
|
symbolValidator,
|
||||||
upperCaseValidator,
|
upperCaseValidator,
|
||||||
} from "@/helpers/validators";
|
} from "@/helpers/validators";
|
||||||
import { registerUser, RegisterUserResponse } from "@/lib/server/register";
|
import { changePassword, sendPassword } from "@/lib/server/password";
|
||||||
|
import { create } from "@zitadel/client";
|
||||||
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -21,36 +23,35 @@ import { Spinner } from "./spinner";
|
|||||||
|
|
||||||
type Inputs =
|
type Inputs =
|
||||||
| {
|
| {
|
||||||
|
code: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
}
|
}
|
||||||
| FieldValues;
|
| FieldValues;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
code?: string;
|
||||||
passwordComplexitySettings: PasswordComplexitySettings;
|
passwordComplexitySettings: PasswordComplexitySettings;
|
||||||
email: string;
|
loginName: string;
|
||||||
firstname: string;
|
userId: string;
|
||||||
lastname: string;
|
|
||||||
organization?: string;
|
organization?: string;
|
||||||
authRequestId?: string;
|
authRequestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SetPasswordForm({
|
export function SetPasswordForm({
|
||||||
passwordComplexitySettings,
|
passwordComplexitySettings,
|
||||||
email,
|
|
||||||
firstname,
|
|
||||||
lastname,
|
|
||||||
organization,
|
organization,
|
||||||
authRequestId,
|
authRequestId,
|
||||||
|
loginName,
|
||||||
|
userId,
|
||||||
|
code,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useTranslations("register");
|
const t = useTranslations("password");
|
||||||
|
|
||||||
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
|
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: email ?? "",
|
code: code ?? "",
|
||||||
firstname: firstname ?? "",
|
|
||||||
lastname: lastname ?? "",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,58 +62,73 @@ export function SetPasswordForm({
|
|||||||
|
|
||||||
async function submitRegister(values: Inputs) {
|
async function submitRegister(values: Inputs) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await registerUser({
|
const changeResponse = await changePassword({
|
||||||
email: email,
|
userId: userId,
|
||||||
firstName: firstname,
|
|
||||||
lastName: lastname,
|
|
||||||
organization: organization,
|
|
||||||
authRequestId: authRequestId,
|
|
||||||
password: values.password,
|
password: values.password,
|
||||||
|
code: values.code,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setError("Could not register user");
|
setError("Could not register user");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response && "error" in response) {
|
if (changeResponse && "error" in changeResponse) {
|
||||||
setError(response.error);
|
setError(changeResponse.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!response) {
|
if (!changeResponse) {
|
||||||
setError("Could not register user");
|
setError("Could not register user");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResponse = response as RegisterUserResponse;
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
const params = new URLSearchParams({ userId: userResponse.userId });
|
if (loginName) {
|
||||||
|
params.append("loginName", loginName);
|
||||||
if (userResponse.factors?.user?.loginName) {
|
|
||||||
params.append("loginName", userResponse.factors.user.loginName);
|
|
||||||
}
|
}
|
||||||
if (organization) {
|
if (organization) {
|
||||||
params.append("organization", organization);
|
params.append("organization", organization);
|
||||||
}
|
}
|
||||||
if (userResponse && userResponse.sessionId) {
|
|
||||||
params.append("sessionId", userResponse.sessionId);
|
const passwordResponse = await sendPassword({
|
||||||
|
loginName,
|
||||||
|
organization,
|
||||||
|
checks: create(ChecksSchema, {
|
||||||
|
password: { password: values.password },
|
||||||
|
}),
|
||||||
|
authRequestId,
|
||||||
|
}).catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setError("Could not verify password");
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (
|
||||||
|
passwordResponse &&
|
||||||
|
"error" in passwordResponse &&
|
||||||
|
passwordResponse.error
|
||||||
|
) {
|
||||||
|
setError(passwordResponse.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip verification for now as it is an app based flow
|
// // skip verification for now as it is an app based flow
|
||||||
// return router.push(`/verify?` + params);
|
// // return router.push(`/verify?` + params);
|
||||||
|
|
||||||
// check for mfa force to continue with mfa setup
|
// // check for mfa force to continue with mfa setup
|
||||||
|
|
||||||
if (authRequestId && userResponse.sessionId) {
|
// if (authRequestId && changeResponse.sessionId) {
|
||||||
if (authRequestId) {
|
// if (authRequestId) {
|
||||||
params.append("authRequest", authRequestId);
|
// params.append("authRequest", authRequestId);
|
||||||
}
|
// }
|
||||||
return router.push(`/login?` + params);
|
// return router.push(`/login?` + params);
|
||||||
} else {
|
// } else {
|
||||||
if (authRequestId) {
|
// if (authRequestId) {
|
||||||
params.append("authRequestId", authRequestId);
|
// params.append("authRequestId", authRequestId);
|
||||||
}
|
// }
|
||||||
return router.push(`/signedin?` + params);
|
// return router.push(`/signedin?` + params);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
@@ -139,6 +155,24 @@ export function SetPasswordForm({
|
|||||||
return (
|
return (
|
||||||
<form className="w-full">
|
<form className="w-full">
|
||||||
<div className="pt-4 grid grid-cols-1 gap-4 mb-4">
|
<div className="pt-4 grid grid-cols-1 gap-4 mb-4">
|
||||||
|
<div className="flex flex-row items-end">
|
||||||
|
<div className="flex-1">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
{...register("code", {
|
||||||
|
required: "This field is required",
|
||||||
|
})}
|
||||||
|
label="Code"
|
||||||
|
error={errors.code?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 mb-1">
|
||||||
|
<Button variant={ButtonVariants.Secondary}>
|
||||||
|
{t("set.resend")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="">
|
<div className="">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="password"
|
type="password"
|
||||||
@@ -147,7 +181,7 @@ export function SetPasswordForm({
|
|||||||
{...register("password", {
|
{...register("password", {
|
||||||
required: "You have to provide a password!",
|
required: "You have to provide a password!",
|
||||||
})}
|
})}
|
||||||
label="Password"
|
label="New Password"
|
||||||
error={errors.password?.message as string}
|
error={errors.password?.message as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,7 +223,7 @@ export function SetPasswordForm({
|
|||||||
onClick={handleSubmit(submitRegister)}
|
onClick={handleSubmit(submitRegister)}
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
{t("password.submit")}
|
{t("set.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
197
apps/login/src/components/set-register-password-form.tsx
Normal file
197
apps/login/src/components/set-register-password-form.tsx
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
lowerCaseValidator,
|
||||||
|
numberValidator,
|
||||||
|
symbolValidator,
|
||||||
|
upperCaseValidator,
|
||||||
|
} from "@/helpers/validators";
|
||||||
|
import { registerUser, RegisterUserResponse } from "@/lib/server/register";
|
||||||
|
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FieldValues, useForm } from "react-hook-form";
|
||||||
|
import { Alert } from "./alert";
|
||||||
|
import { BackButton } from "./back-button";
|
||||||
|
import { Button, ButtonVariants } from "./button";
|
||||||
|
import { TextInput } from "./input";
|
||||||
|
import { PasswordComplexity } from "./password-complexity";
|
||||||
|
import { Spinner } from "./spinner";
|
||||||
|
|
||||||
|
type Inputs =
|
||||||
|
| {
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
| FieldValues;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
passwordComplexitySettings: PasswordComplexitySettings;
|
||||||
|
email: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
organization?: string;
|
||||||
|
authRequestId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SetRegisterPasswordForm({
|
||||||
|
passwordComplexitySettings,
|
||||||
|
email,
|
||||||
|
firstname,
|
||||||
|
lastname,
|
||||||
|
organization,
|
||||||
|
authRequestId,
|
||||||
|
}: Props) {
|
||||||
|
const t = useTranslations("register");
|
||||||
|
|
||||||
|
const { register, handleSubmit, watch, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
defaultValues: {
|
||||||
|
email: email ?? "",
|
||||||
|
firstname: firstname ?? "",
|
||||||
|
lastname: lastname ?? "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function submitRegister(values: Inputs) {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await registerUser({
|
||||||
|
email: email,
|
||||||
|
firstName: firstname,
|
||||||
|
lastName: lastname,
|
||||||
|
organization: organization,
|
||||||
|
authRequestId: authRequestId,
|
||||||
|
password: values.password,
|
||||||
|
}).catch(() => {
|
||||||
|
setError("Could not register user");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response && "error" in response) {
|
||||||
|
setError(response.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
setError("Could not register user");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = response as RegisterUserResponse;
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ userId: userResponse.userId });
|
||||||
|
|
||||||
|
if (userResponse.factors?.user?.loginName) {
|
||||||
|
params.append("loginName", userResponse.factors.user.loginName);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
if (userResponse && userResponse.sessionId) {
|
||||||
|
params.append("sessionId", userResponse.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip verification for now as it is an app based flow
|
||||||
|
// return router.push(`/verify?` + params);
|
||||||
|
|
||||||
|
// check for mfa force to continue with mfa setup
|
||||||
|
|
||||||
|
if (authRequestId && userResponse.sessionId) {
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequest", authRequestId);
|
||||||
|
}
|
||||||
|
return router.push(`/login?` + params);
|
||||||
|
} else {
|
||||||
|
if (authRequestId) {
|
||||||
|
params.append("authRequestId", authRequestId);
|
||||||
|
}
|
||||||
|
return router.push(`/signedin?` + params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { errors } = formState;
|
||||||
|
|
||||||
|
const watchPassword = watch("password", "");
|
||||||
|
const watchConfirmPassword = watch("confirmPassword", "");
|
||||||
|
|
||||||
|
const hasMinLength =
|
||||||
|
passwordComplexitySettings &&
|
||||||
|
watchPassword?.length >= passwordComplexitySettings.minLength;
|
||||||
|
const hasSymbol = symbolValidator(watchPassword);
|
||||||
|
const hasNumber = numberValidator(watchPassword);
|
||||||
|
const hasUppercase = upperCaseValidator(watchPassword);
|
||||||
|
const hasLowercase = lowerCaseValidator(watchPassword);
|
||||||
|
|
||||||
|
const policyIsValid =
|
||||||
|
passwordComplexitySettings &&
|
||||||
|
(passwordComplexitySettings.requiresLowercase ? hasLowercase : true) &&
|
||||||
|
(passwordComplexitySettings.requiresNumber ? hasNumber : true) &&
|
||||||
|
(passwordComplexitySettings.requiresUppercase ? hasUppercase : true) &&
|
||||||
|
(passwordComplexitySettings.requiresSymbol ? hasSymbol : true) &&
|
||||||
|
hasMinLength;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="w-full">
|
||||||
|
<div className="pt-4 grid grid-cols-1 gap-4 mb-4">
|
||||||
|
<div className="">
|
||||||
|
<TextInput
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
{...register("password", {
|
||||||
|
required: "You have to provide a password!",
|
||||||
|
})}
|
||||||
|
label="Password"
|
||||||
|
error={errors.password?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<TextInput
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autoComplete="new-password"
|
||||||
|
{...register("confirmPassword", {
|
||||||
|
required: "This field is required",
|
||||||
|
})}
|
||||||
|
label="Confirm Password"
|
||||||
|
error={errors.confirmPassword?.message as string}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{passwordComplexitySettings && (
|
||||||
|
<PasswordComplexity
|
||||||
|
passwordComplexitySettings={passwordComplexitySettings}
|
||||||
|
password={watchPassword}
|
||||||
|
equals={!!watchPassword && watchPassword === watchConfirmPassword}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && <Alert>{error}</Alert>}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||||
|
<BackButton />
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={
|
||||||
|
loading ||
|
||||||
|
!policyIsValid ||
|
||||||
|
!formState.isValid ||
|
||||||
|
watchPassword !== watchConfirmPassword
|
||||||
|
}
|
||||||
|
onClick={handleSubmit(submitRegister)}
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
{t("password.submit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "@zitadel/client/v2";
|
} from "@zitadel/client/v2";
|
||||||
import { createServerTransport } from "@zitadel/node";
|
import { createServerTransport } from "@zitadel/node";
|
||||||
import { getSessionCookieById } from "./cookies";
|
import { getSessionCookieById } from "./cookies";
|
||||||
|
import { getSession } from "./zitadel";
|
||||||
|
|
||||||
const transport = (token: string) =>
|
const transport = (token: string) =>
|
||||||
createServerTransport(token, {
|
createServerTransport(token, {
|
||||||
@@ -19,26 +20,46 @@ const sessionService = (sessionId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const userService = (sessionId: string) => {
|
const myUserService = (sessionToken: string) => {
|
||||||
return getSessionCookieById({ sessionId }).then((session) => {
|
return createUserServiceClient(transport(sessionToken));
|
||||||
return createUserServiceClient(transport(session.token));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function setPassword({
|
export async function setMyPassword({
|
||||||
sessionId,
|
sessionId,
|
||||||
userId,
|
|
||||||
password,
|
password,
|
||||||
}: {
|
}: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userId: string;
|
|
||||||
password: string;
|
password: string;
|
||||||
}) {
|
}) {
|
||||||
return (await userService(sessionId)).setPassword(
|
const sessionCookie = await getSessionCookieById({ sessionId });
|
||||||
{
|
|
||||||
userId,
|
const { session } = await getSession({
|
||||||
newPassword: { password, changeRequired: false },
|
sessionId: sessionCookie.id,
|
||||||
},
|
sessionToken: sessionCookie.token,
|
||||||
{},
|
});
|
||||||
);
|
|
||||||
|
if (!session) {
|
||||||
|
return { error: "Could not load session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await myUserService(sessionCookie.token);
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "No user id found in session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return service
|
||||||
|
.setPassword(
|
||||||
|
{
|
||||||
|
userId: session.factors.user.id,
|
||||||
|
newPassword: { password, changeRequired: false },
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.code === 7) {
|
||||||
|
return { error: "Session is not valid." };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
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 { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@@ -135,6 +136,25 @@ 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) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: session.factors?.user?.loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
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(
|
||||||
session.factors?.user?.id,
|
session.factors?.user?.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,15 +5,20 @@ import {
|
|||||||
setSessionAndUpdateCookie,
|
setSessionAndUpdateCookie,
|
||||||
} from "@/lib/server/cookie";
|
} from "@/lib/server/cookie";
|
||||||
import {
|
import {
|
||||||
|
getUserByID,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
listUsers,
|
listUsers,
|
||||||
passwordReset,
|
passwordReset,
|
||||||
|
setPassword,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import {
|
import {
|
||||||
Checks,
|
Checks,
|
||||||
ChecksSchema,
|
ChecksSchema,
|
||||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
|
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieByLoginName } from "../cookies";
|
||||||
|
|
||||||
type ResetPasswordCommand = {
|
type ResetPasswordCommand = {
|
||||||
@@ -44,6 +49,7 @@ 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) {
|
||||||
@@ -55,13 +61,18 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let session;
|
let session;
|
||||||
|
let user: User;
|
||||||
if (!sessionCookie) {
|
if (!sessionCookie) {
|
||||||
const users = await listUsers({
|
const users = await listUsers({
|
||||||
loginName: command.loginName,
|
loginName: command.loginName,
|
||||||
organizationId: command.organization,
|
organizationId: command.organization,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(users);
|
||||||
|
|
||||||
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
||||||
|
user = users.result[0];
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
const checks = create(ChecksSchema, {
|
||||||
user: { search: { case: "userId", value: users.result[0].userId } },
|
user: { search: { case: "userId", value: users.result[0].userId } },
|
||||||
password: { password: command.checks.password?.password },
|
password: { password: command.checks.password?.password },
|
||||||
@@ -83,6 +94,18 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
undefined,
|
undefined,
|
||||||
command.authRequestId,
|
command.authRequestId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not create session for user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||||
|
|
||||||
|
if (!userResponse.user) {
|
||||||
|
return { error: "Could not find user" };
|
||||||
|
}
|
||||||
|
|
||||||
|
user = userResponse.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session?.factors?.user?.id || !sessionCookie) {
|
if (!session?.factors?.user?.id || !sessionCookie) {
|
||||||
@@ -100,10 +123,165 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const submitted = {
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
factors: session.factors,
|
factors: session.factors,
|
||||||
challenges: session.challenges,
|
challenges: session.challenges,
|
||||||
authMethods,
|
authMethods,
|
||||||
|
userState: user.state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!submitted ||
|
||||||
|
!submitted.authMethods ||
|
||||||
|
!submitted.factors?.user?.loginName
|
||||||
|
) {
|
||||||
|
return { error: "Could not verify password!" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableSecondFactors = submitted?.authMethods?.filter(
|
||||||
|
(m: AuthenticationMethodType) =>
|
||||||
|
m !== AuthenticationMethodType.PASSWORD &&
|
||||||
|
m !== AuthenticationMethodType.PASSKEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availableSecondFactors?.length == 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: submitted.factors?.user.loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.authRequestId) {
|
||||||
|
params.append("authRequestId", command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.append("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = availableSecondFactors[0];
|
||||||
|
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||||
|
if (factor === AuthenticationMethodType.TOTP) {
|
||||||
|
return redirect(`/otp/time-based?` + params);
|
||||||
|
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||||
|
return redirect(`/otp/sms?` + params);
|
||||||
|
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||||
|
return redirect(`/otp/email?` + params);
|
||||||
|
} else if (factor === AuthenticationMethodType.U2F) {
|
||||||
|
return redirect(`/u2f?` + params);
|
||||||
|
}
|
||||||
|
} else if (availableSecondFactors?.length >= 1) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: submitted.factors.user.loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.authRequestId) {
|
||||||
|
params.append("authRequestId", command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.append("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(`/mfa?` + params);
|
||||||
|
} else if (submitted.userState === UserState.INITIAL) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: submitted.factors.user.loginName,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.authRequestId) {
|
||||||
|
params.append("authRequestId", command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.append("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(`/password/change?` + params);
|
||||||
|
} else if (command.forceMfa && !availableSecondFactors.length) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: submitted.factors.user.loginName,
|
||||||
|
force: "true", // this defines if the mfa is forced in the settings
|
||||||
|
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.authRequestId) {
|
||||||
|
params.append("authRequestId", command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.append("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: provide a way to setup passkeys on mfa page?
|
||||||
|
return redirect(`/mfa/set?` + params);
|
||||||
|
}
|
||||||
|
// TODO: implement passkey setup
|
||||||
|
|
||||||
|
// else if (
|
||||||
|
// submitted.factors &&
|
||||||
|
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
||||||
|
// promptPasswordless && // if explicitly prompted due policy
|
||||||
|
// !isAlternative // escaped if password was used as an alternative method
|
||||||
|
// ) {
|
||||||
|
// const params = new URLSearchParams({
|
||||||
|
// loginName: submitted.factors.user.loginName,
|
||||||
|
// prompt: "true",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (authRequestId) {
|
||||||
|
// params.append("authRequestId", authRequestId);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (organization) {
|
||||||
|
// params.append("organization", organization);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return router.push(`/passkey/set?` + params);
|
||||||
|
// }
|
||||||
|
else if (command.authRequestId && submitted.sessionId) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
sessionId: submitted.sessionId,
|
||||||
|
authRequest: command.authRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.append("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(`/login?` + params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// without OIDC flow
|
||||||
|
const params = new URLSearchParams(
|
||||||
|
command.authRequestId
|
||||||
|
? {
|
||||||
|
loginName: submitted.factors.user.loginName,
|
||||||
|
authRequestId: command.authRequestId,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
loginName: submitted.factors.user.loginName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.append("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(`/signedin?` + params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(command: {
|
||||||
|
code: string;
|
||||||
|
userId: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
|
// check for init state
|
||||||
|
const { user } = await getUserByID(command.userId);
|
||||||
|
|
||||||
|
if (!user || user.userId !== command.userId) {
|
||||||
|
return { error: "Could not send Password Reset Link" };
|
||||||
|
}
|
||||||
|
const userId = user.userId;
|
||||||
|
|
||||||
|
return setPassword(userId, command.password, command.code);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||||
|
import { NotificationType } from "@zitadel/proto/zitadel/user/v2/password_pb";
|
||||||
import {
|
import {
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQuerySchema,
|
SearchQuerySchema,
|
||||||
@@ -495,6 +496,32 @@ export async function passwordReset(userId: string) {
|
|||||||
return userService.passwordReset(
|
return userService.passwordReset(
|
||||||
{
|
{
|
||||||
userId,
|
userId,
|
||||||
|
medium: {
|
||||||
|
case: "sendLink",
|
||||||
|
value: {
|
||||||
|
notificationType: NotificationType.Email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setPassword(
|
||||||
|
userId: string,
|
||||||
|
password: string,
|
||||||
|
code: string,
|
||||||
|
) {
|
||||||
|
return userService.setPassword(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
newPassword: {
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
case: "verificationCode",
|
||||||
|
value: code,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user