From f07a64f4e52c4e9299332f8603bc49897215cc88 Mon Sep 17 00:00:00 2001 From: peintnermax Date: Fri, 13 Sep 2024 14:01:03 +0200 Subject: [PATCH] loginname to ignore usernames, description --- apps/login/readme.md | 30 ++++++ apps/login/src/app/(login)/register/page.tsx | 4 + apps/login/src/lib/server/loginname.ts | 97 ++++++++++++-------- apps/login/src/ui/UsernameForm.tsx | 13 +-- 4 files changed, 96 insertions(+), 48 deletions(-) diff --git a/apps/login/readme.md b/apps/login/readme.md index d7a95866516..fe10c5e8cea 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -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?)` diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index f010f229c2e..700173e9dc7 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -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); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 5831d3eb21f..3701fa7de2c 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -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"); } diff --git a/apps/login/src/ui/UsernameForm.tsx b/apps/login/src/ui/UsernameForm.tsx index 8f87d9a4587..68beacf7e9a 100644 --- a/apps/login/src/ui/UsernameForm.tsx +++ b/apps/login/src/ui/UsernameForm.tsx @@ -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 && } continue