mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 08:23:16 +00:00
loginname to ignore usernames, description
This commit is contained in:
@@ -43,3 +43,33 @@ This is going to be our next UI for the hosted login. It's based on Next.js 13 a
|
|||||||
passkey-- notVerified --> verify
|
passkey-- notVerified --> verify
|
||||||
verify --> B[signedin]
|
verify --> B[signedin]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### /loginname
|
||||||
|
|
||||||
|
This page shows a loginname field and Identity Providers to login or register.
|
||||||
|
If `loginSettings(org?).allowRegister` is `true`, if will also show a link to jump to /register
|
||||||
|
|
||||||
|
- `getLoginSettings(org?)`
|
||||||
|
- `getLegalAndSupportSettings(org?)`
|
||||||
|
- `getIdentityProviders(org?)`
|
||||||
|
- `getBrandingSettings(org?)`
|
||||||
|
- `getActiveIdentityProviders(org?)`
|
||||||
|
- `startIdentityProviderFlow`
|
||||||
|
- `listUsers(org?)`
|
||||||
|
- `listAuthenticationMethodTypes`
|
||||||
|
|
||||||
|
After a loginname is entered, a `listUsers` request is made using the loginName query to identify already registered users.
|
||||||
|
|
||||||
|
If only one user is found, we query `listAuthenticationMethodTypes` to identify future steps.
|
||||||
|
If no authentication methods are found, we render an error stating: _User has no available authentication methods._
|
||||||
|
Now if only one method is found, we continue with the corresponding step (/password, /passkey/login).
|
||||||
|
If multiple methods are set, we prefer passkeys over any other method, so we redirect to /passkey, second option is IDP, and third is password.
|
||||||
|
If password is the next step, we check `loginSettings.passkeysType` for PasskeysType.ALLOWED, and prompt the user to setup passkeys afterwards.
|
||||||
|
|
||||||
|
If no user is found, we check whether registering is allowed using `loginSettings.allowRegister`.
|
||||||
|
If `loginSettings?.allowUsernamePassword` is not allowed we continue to check for available IDPs. If a single IDP is available, we directly redirect the user to signup.
|
||||||
|
|
||||||
|
If no single IDP is set, we check for `loginSettings.allowUsernamePassword` and redirect the user to /register page.
|
||||||
|
If no previous condition is met, we check whether `loginSettings?.ignoreUnknownUsernames` is `false` and in such case, we return a user not found error. If not, we redirect to the /password page, regardless (to not leak information about a registered user).
|
||||||
|
|
||||||
|
> NOTE: We ignore `loginSettings.allowExternalIdp` as the information whether IDPs are available comes as response from `getActiveIdentityProviders(org?)`
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export default async function Page({
|
|||||||
const { firstname, lastname, email, organization, authRequestId } =
|
const { firstname, lastname, email, organization, authRequestId } =
|
||||||
searchParams;
|
searchParams;
|
||||||
|
|
||||||
|
if (!organization) {
|
||||||
|
// TODO: get default organization
|
||||||
|
}
|
||||||
|
|
||||||
const setPassword = !!(firstname && lastname && email);
|
const setPassword = !!(firstname && lastname && email);
|
||||||
|
|
||||||
const legal = await getLegalAndSupportSettings(organization);
|
const legal = await getLegalAndSupportSettings(organization);
|
||||||
|
|||||||
@@ -28,6 +28,45 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
|
|
||||||
const loginSettings = await getLoginSettings(command.organization);
|
const loginSettings = await getLoginSettings(command.organization);
|
||||||
|
|
||||||
|
const redirectUserToSingleIDPIfAvailable = async () => {
|
||||||
|
const identityProviders = await getActiveIdentityProviders(
|
||||||
|
command.organization,
|
||||||
|
).then((resp) => {
|
||||||
|
return resp.identityProviders;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (identityProviders.length === 1) {
|
||||||
|
const host = headers().get("host");
|
||||||
|
const identityProviderType = identityProviders[0].type;
|
||||||
|
|
||||||
|
const provider = idpTypeToSlug(identityProviderType);
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (command.authRequestId) {
|
||||||
|
params.set("authRequestId", command.authRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
params.set("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await startIdentityProviderFlow({
|
||||||
|
idpId: identityProviders[0].id,
|
||||||
|
urls: {
|
||||||
|
successUrl:
|
||||||
|
`${host}/idp/${provider}/success?` + new URLSearchParams(params),
|
||||||
|
failureUrl:
|
||||||
|
`${host}/idp/${provider}/failure?` + new URLSearchParams(params),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp?.nextStep.case === "authUrl") {
|
||||||
|
return redirect(resp.nextStep.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
||||||
const userId = users.result[0].userId;
|
const userId = users.result[0].userId;
|
||||||
const session = await createSessionForUserIdAndUpdateCookie(
|
const session = await createSessionForUserIdAndUpdateCookie(
|
||||||
@@ -115,13 +154,14 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
|
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
|
||||||
) {
|
) {
|
||||||
// TODO: redirect user to idp
|
// TODO: redirect user to idp
|
||||||
|
await redirectUserToSingleIDPIfAvailable();
|
||||||
} else if (
|
} else if (
|
||||||
methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)
|
methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)
|
||||||
) {
|
) {
|
||||||
// user has no passkey setup and login settings allow passkeys
|
// user has no passkey setup and login settings allow passkeys
|
||||||
const paramsPasswordDefault: any = { loginName: command.loginName };
|
const paramsPasswordDefault: any = { loginName: command.loginName };
|
||||||
|
|
||||||
if (loginSettings?.passkeysType === 1) {
|
if (loginSettings?.passkeysType === PasskeysType.ALLOWED) {
|
||||||
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,42 +186,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
|
|
||||||
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) {
|
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) {
|
||||||
// TODO redirect to loginname page with idp hint
|
// TODO redirect to loginname page with idp hint
|
||||||
const identityProviders = await getActiveIdentityProviders(
|
await redirectUserToSingleIDPIfAvailable();
|
||||||
command.organization,
|
|
||||||
).then((resp) => {
|
|
||||||
return resp.identityProviders;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (identityProviders.length === 1) {
|
|
||||||
const host = headers().get("host");
|
|
||||||
const identityProviderType = identityProviders[0].type;
|
|
||||||
|
|
||||||
const provider = idpTypeToSlug(identityProviderType);
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (command.authRequestId) {
|
|
||||||
params.set("authRequestId", command.authRequestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.organization) {
|
|
||||||
params.set("organization", command.organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await startIdentityProviderFlow({
|
|
||||||
idpId: identityProviders[0].id,
|
|
||||||
urls: {
|
|
||||||
successUrl:
|
|
||||||
`${host}/idp/${provider}/success?` + new URLSearchParams(params),
|
|
||||||
failureUrl:
|
|
||||||
`${host}/idp/${provider}/failure?` + new URLSearchParams(params),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp?.nextStep.case === "authUrl") {
|
|
||||||
return redirect(resp.nextStep.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw Error("Could not find user");
|
throw Error("Could not find user");
|
||||||
} else if (
|
} else if (
|
||||||
@@ -205,5 +210,23 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
return redirect(registerUrl);
|
return redirect(registerUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loginSettings?.ignoreUnknownUsernames) {
|
||||||
|
const paramsPasswordDefault: any = { loginName: command.loginName };
|
||||||
|
|
||||||
|
if (loginSettings?.passkeysType === PasskeysType.ALLOWED) {
|
||||||
|
paramsPasswordDefault.promptPasswordless = `true`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.authRequestId) {
|
||||||
|
paramsPasswordDefault.authRequestId = command.authRequestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.organization) {
|
||||||
|
paramsPasswordDefault.organization = command.organization;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect("/password?" + new URLSearchParams(paramsPasswordDefault));
|
||||||
|
}
|
||||||
|
|
||||||
throw Error("Could not find user");
|
throw Error("Could not find user");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,17 +59,10 @@ export default function UsernameForm({
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setLoginNameAndGetAuthMethods(
|
|
||||||
values: Inputs,
|
|
||||||
organization?: string,
|
|
||||||
) {
|
|
||||||
const response = await submitLoginName(values, organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (submit && loginName) {
|
if (submit && loginName) {
|
||||||
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
|
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
|
||||||
setLoginNameAndGetAuthMethods({ loginName }, organization);
|
submitLoginName({ loginName }, organization);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -120,9 +113,7 @@ export default function UsernameForm({
|
|||||||
className="self-end"
|
className="self-end"
|
||||||
variant={ButtonVariants.Primary}
|
variant={ButtonVariants.Primary}
|
||||||
disabled={loading || !formState.isValid}
|
disabled={loading || !formState.isValid}
|
||||||
onClick={handleSubmit((e) =>
|
onClick={handleSubmit((e) => submitLoginName(e, organization))}
|
||||||
setLoginNameAndGetAuthMethods(e, organization),
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user