Files
zitadel/apps/login/src/lib/server/loginname.ts

338 lines
11 KiB
TypeScript
Raw Normal View History

2024-08-29 16:25:44 +02:00
"use server";
2024-09-18 14:13:04 +02:00
import { create } from "@zitadel/client";
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
2024-10-15 16:29:16 +02:00
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
2024-09-11 15:27:31 +02:00
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
2024-08-29 16:25:44 +02:00
import { headers } from "next/headers";
2024-09-05 13:48:33 +02:00
import { redirect } from "next/navigation";
2024-10-10 17:05:43 +02:00
import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
2024-08-29 16:25:44 +02:00
import {
2024-10-11 08:32:04 +02:00
getActiveIdentityProviders,
2024-10-10 17:05:43 +02:00
getIDPByID,
2024-08-29 16:25:44 +02:00
getLoginSettings,
2024-09-13 14:47:33 +02:00
getOrgsByDomain,
2024-08-29 16:25:44 +02:00
listAuthenticationMethodTypes,
2024-10-10 17:05:43 +02:00
listIDPLinks,
2024-08-29 16:25:44 +02:00
listUsers,
startIdentityProviderFlow,
} from "../zitadel";
import { createSessionAndUpdateCookie } from "./cookie";
2024-08-29 16:25:44 +02:00
2024-08-30 09:52:42 +02:00
export type SendLoginnameCommand = {
2024-08-29 16:25:44 +02:00
loginName: string;
authRequestId?: string;
organization?: string;
};
2024-09-13 14:47:33 +02:00
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
2024-09-05 13:38:03 +02:00
export async function sendLoginname(command: SendLoginnameCommand) {
2024-08-29 16:25:44 +02:00
const users = await listUsers({
2024-09-11 09:27:04 +02:00
loginName: command.loginName,
2024-09-05 13:38:03 +02:00
organizationId: command.organization,
2024-08-29 16:25:44 +02:00
});
2024-09-11 15:27:31 +02:00
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);
}
}
};
2024-10-10 17:05:43 +02:00
const redirectUserToIDP = async (userId: string) => {
const identityProviders = await listIDPLinks(userId).then((resp) => {
return resp.result;
});
if (identityProviders.length === 1) {
const host = headers().get("host");
const identityProviderId = identityProviders[0].idpId;
const idp = await getIDPByID(identityProviderId);
const idpType = idp?.type;
if (!idp || !idpType) {
throw new Error("Could not find identity provider");
}
const identityProviderType = idpTypeToIdentityProviderType(idpType);
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: idp.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);
}
}
};
2024-08-29 16:25:44 +02:00
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
const userId = users.result[0].userId;
2024-09-18 14:13:04 +02:00
const checks = create(ChecksSchema, {
user: { search: { case: "userId", value: userId } },
});
const session = await createSessionAndUpdateCookie(
checks,
2024-08-29 16:25:44 +02:00
undefined,
2024-09-05 13:38:03 +02:00
command.authRequestId,
2024-08-29 16:25:44 +02:00
);
2024-08-30 09:52:42 +02:00
if (!session.factors?.user?.id) {
return { error: "Could not create session for user" };
2024-08-29 16:25:44 +02:00
}
2024-10-15 16:29:16 +02:00
if (users.result[0].state === UserState.INITIAL) {
const params = new URLSearchParams({
loginName: session.factors?.user?.loginName,
2024-10-23 14:28:33 +02:00
initial: "true", // this does not require a code to be set
2024-10-15 16:29:16 +02:00
});
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);
}
2024-08-29 16:25:44 +02:00
const methods = await listAuthenticationMethodTypes(
session.factors?.user?.id,
);
2024-09-11 15:27:31 +02:00
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
if (
users.result[0].type.case === "human" &&
users.result[0].type.value.email &&
!users.result[0].type.value.email.isVerified
) {
const paramsVerify = new URLSearchParams({
loginName: session.factors?.user?.loginName,
userId: session.factors?.user?.id, // verify needs user id
});
if (command.organization || session.factors?.user?.organizationId) {
paramsVerify.append(
"organization",
command.organization ?? session.factors?.user?.organizationId,
);
}
if (command.authRequestId) {
paramsVerify.append("authRequestId", command.authRequestId);
}
redirect("/verify?" + paramsVerify);
}
return {
error:
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
};
2024-09-11 15:27:31 +02:00
}
if (methods.authMethodTypes.length == 1) {
const method = methods.authMethodTypes[0];
switch (method) {
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
const paramsPassword: any = {
loginName: session.factors?.user?.loginName,
};
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
if (command.organization || session.factors?.user?.organizationId) {
paramsPassword.organization =
command.organization ?? session.factors?.user?.organizationId;
}
if (command.authRequestId) {
paramsPassword.authRequestId = command.authRequestId;
}
return redirect("/password?" + new URLSearchParams(paramsPassword));
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
const paramsPasskey: any = { loginName: command.loginName };
if (command.authRequestId) {
paramsPasskey.authRequestId = command.authRequestId;
}
if (command.organization || session.factors?.user?.organizationId) {
paramsPasskey.organization =
command.organization ?? session.factors?.user?.organizationId;
}
2024-09-16 12:04:43 +02:00
return redirect("/passkey?" + new URLSearchParams(paramsPasskey));
2024-09-11 15:27:31 +02:00
}
} else {
// prefer passkey in favor of other methods
if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSKEY)) {
const passkeyParams: any = {
loginName: command.loginName,
altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option
};
if (command.authRequestId) {
passkeyParams.authRequestId = command.authRequestId;
}
if (command.organization || session.factors?.user?.organizationId) {
passkeyParams.organization =
command.organization ?? session.factors?.user?.organizationId;
}
2024-09-16 12:04:43 +02:00
return redirect("/passkey?" + new URLSearchParams(passkeyParams));
2024-09-11 15:27:31 +02:00
} else if (
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
) {
2024-10-10 17:05:43 +02:00
await redirectUserToIDP(userId);
2024-09-11 15:27:31 +02:00
} else if (
methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)
) {
// user has no passkey setup and login settings allow passkeys
const paramsPasswordDefault: any = { loginName: command.loginName };
if (command.authRequestId) {
paramsPasswordDefault.authRequestId = command.authRequestId;
}
if (command.organization || session.factors?.user?.organizationId) {
paramsPasswordDefault.organization =
command.organization ?? session.factors?.user?.organizationId;
}
return redirect(
"/password?" + new URLSearchParams(paramsPasswordDefault),
);
}
}
2024-08-29 16:25:44 +02:00
}
// user not found, check if register is enabled on organization
if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) {
2024-09-13 14:14:09 +02:00
// TODO: do we need to handle login hints for IDPs here?
await redirectUserToSingleIDPIfAvailable();
2024-09-11 13:44:15 +02:00
return { error: "Could not find user" };
2024-08-29 16:25:44 +02:00
} else if (
loginSettings?.allowRegister &&
loginSettings?.allowUsernamePassword
) {
2024-09-13 14:47:33 +02:00
let orgToRegisterOn: string | undefined = command.organization;
if (
2024-09-18 16:41:30 +02:00
!loginSettings?.ignoreUnknownUsernames &&
2024-09-13 14:47:33 +02:00
!orgToRegisterOn &&
command.loginName &&
ORG_SUFFIX_REGEX.test(command.loginName)
) {
const matched = ORG_SUFFIX_REGEX.exec(command.loginName);
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
const orgs = await getOrgsByDomain(suffix);
const orgToCheckForDiscovery =
orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
const orgLoginSettings = await getLoginSettings(orgToCheckForDiscovery);
if (orgLoginSettings?.allowDomainDiscovery) {
orgToRegisterOn = orgToCheckForDiscovery;
}
}
2024-09-18 16:41:30 +02:00
// do not register user if ignoreUnknownUsernames is set
if (orgToRegisterOn && !loginSettings?.ignoreUnknownUsernames) {
2024-09-18 14:13:04 +02:00
const params = new URLSearchParams({ organization: orgToRegisterOn });
2024-08-29 16:25:44 +02:00
2024-09-18 14:13:04 +02:00
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
}
if (command.loginName) {
params.set("loginName", command.loginName);
}
2024-08-29 16:25:44 +02:00
2024-09-18 14:13:04 +02:00
return redirect("/register?" + params);
}
2024-08-29 16:25:44 +02:00
}
if (loginSettings?.ignoreUnknownUsernames) {
2024-09-18 14:13:04 +02:00
const paramsPasswordDefault = new URLSearchParams({
loginName: command.loginName,
});
if (command.authRequestId) {
2024-09-18 14:13:04 +02:00
paramsPasswordDefault.append("authRequestId", command.authRequestId);
}
if (command.organization) {
2024-09-18 14:13:04 +02:00
paramsPasswordDefault.append("organization", command.organization);
}
2024-09-18 14:13:04 +02:00
return redirect("/password?" + paramsPasswordDefault);
}
2024-09-18 14:13:04 +02:00
// fallbackToPassword
return { error: "Could not find user" };
2024-08-29 16:25:44 +02:00
}