loginname to ignore usernames, description

This commit is contained in:
peintnermax
2024-09-13 14:01:03 +02:00
parent a0c67aae2a
commit f07a64f4e5
4 changed files with 96 additions and 48 deletions

View File

@@ -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
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?)`

View File

@@ -15,6 +15,10 @@ export default async function Page({
const { firstname, lastname, email, organization, authRequestId } =
searchParams;
if (!organization) {
// TODO: get default organization
}
const setPassword = !!(firstname && lastname && email);
const legal = await getLegalAndSupportSettings(organization);

View File

@@ -28,6 +28,45 @@ export async function sendLoginname(command: SendLoginnameCommand) {
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) {
const userId = users.result[0].userId;
const session = await createSessionForUserIdAndUpdateCookie(
@@ -115,13 +154,14 @@ export async function sendLoginname(command: SendLoginnameCommand) {
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
) {
// TODO: redirect user to idp
await redirectUserToSingleIDPIfAvailable();
} else if (
methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)
) {
// user has no passkey setup and login settings allow passkeys
const paramsPasswordDefault: any = { loginName: command.loginName };
if (loginSettings?.passkeysType === 1) {
if (loginSettings?.passkeysType === PasskeysType.ALLOWED) {
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
}
@@ -146,42 +186,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) {
// TODO redirect to loginname page with idp hint
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);
}
}
await redirectUserToSingleIDPIfAvailable();
throw Error("Could not find user");
} else if (
@@ -205,5 +210,23 @@ export async function sendLoginname(command: SendLoginnameCommand) {
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");
}

View File

@@ -59,17 +59,10 @@ export default function UsernameForm({
return res;
}
async function setLoginNameAndGetAuthMethods(
values: Inputs,
organization?: string,
) {
const response = await submitLoginName(values, organization);
}
useEffect(() => {
if (submit && loginName) {
// 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"
variant={ButtonVariants.Primary}
disabled={loading || !formState.isValid}
onClick={handleSubmit((e) =>
setLoginNameAndGetAuthMethods(e, organization),
)}
onClick={handleSubmit((e) => submitLoginName(e, organization))}
>
{loading && <Spinner className="h-5 w-5 mr-2" />}
continue