mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 22:02:26 +00:00
Merge pull request #167 from zitadel/qa
fix: general improvements and fixes
This commit is contained in:
@@ -1,3 +1,45 @@
|
||||
# ZITADEL Login UI
|
||||
|
||||
This is going to be our next UI for the hosted login. It's based on Next.js 13 and its introduced `app/` directory.
|
||||
|
||||
## Flow Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> register
|
||||
A[Start] --> accounts
|
||||
A[Start] --> loginname
|
||||
A[Start] --> register
|
||||
A[Start] -- signInWithIDP --> idp
|
||||
idp --> idp-success
|
||||
idp --> idp-failure
|
||||
idp-success --> B[signedin]
|
||||
idp-failure --> loginname
|
||||
loginname --> password
|
||||
loginname -- hasPasskey --> passkey
|
||||
loginname -- allowRegister --> register
|
||||
passkey-add --passwordAllowed --> password
|
||||
passkey -- hasPassword --> password
|
||||
passkey --> B[signedin]
|
||||
password -- hasMFA --> mfa
|
||||
password -- allowPasskeys --> passkey-add
|
||||
mfa --> otp
|
||||
otp --> B[signedin]
|
||||
mfa--> u2f
|
||||
u2f -->B[signedin]
|
||||
register --> passkey-add
|
||||
register --> password-set
|
||||
password-set --> B[signedin]
|
||||
passkey-add --> B[signedin]
|
||||
password --> B[signedin]
|
||||
password-- forceMFA -->mfaset
|
||||
mfaset --> u2fset
|
||||
mfaset --> otpset
|
||||
u2fset --> B[signedin]
|
||||
otpset --> B[signedin]
|
||||
accounts--> loginname
|
||||
password -- not verified yet -->verify
|
||||
register-- withpassword -->verify
|
||||
passkey-- notVerified --> verify
|
||||
verify --> B[signedin]
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PROVIDER_MAPPING } from "@/lib/idp";
|
||||
import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp";
|
||||
import {
|
||||
addIDPLink,
|
||||
createUser,
|
||||
@@ -51,11 +51,17 @@ export default async function Page({
|
||||
const idp = await getIDPByID(idpInformation.idpId);
|
||||
const options = idp?.config?.options;
|
||||
|
||||
if (!idp) {
|
||||
throw new Error("IDP not found");
|
||||
}
|
||||
|
||||
const providerType = idpTypeToIdentityProviderType(idp.type);
|
||||
|
||||
// search for potential user via username, then link
|
||||
if (options?.isLinkingAllowed) {
|
||||
let foundUser;
|
||||
const email =
|
||||
PROVIDER_MAPPING[provider](idpInformation).email?.email;
|
||||
PROVIDER_MAPPING[providerType](idpInformation).email?.email;
|
||||
|
||||
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
|
||||
foundUser = await listUsers({ email }).then((response) => {
|
||||
@@ -118,7 +124,7 @@ export default async function Page({
|
||||
}
|
||||
|
||||
if (options?.isCreationAllowed && options.isAutoCreation) {
|
||||
const newUser = await createUser(provider, idpInformation);
|
||||
const newUser = await createUser(providerType, idpInformation);
|
||||
|
||||
if (newUser) {
|
||||
return (
|
||||
|
||||
@@ -45,7 +45,6 @@ export default async function Page({
|
||||
<p className="ztdl-p">Enter your login data.</p>
|
||||
|
||||
<UsernameForm
|
||||
loginSettings={loginSettings}
|
||||
loginName={loginName}
|
||||
authRequestId={authRequestId}
|
||||
organization={organization}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
import { Button, ButtonVariants } from "@/ui/Button";
|
||||
import { TextInput } from "@/ui/Input";
|
||||
import UserAvatar from "@/ui/UserAvatar";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h1>Password</h1>
|
||||
<p className="ztdl-p mb-6 block">Enter your password.</p>
|
||||
|
||||
<UserAvatar
|
||||
showDropdown
|
||||
displayName="Max Peintner"
|
||||
loginName="max@zitadel.com"
|
||||
></UserAvatar>
|
||||
|
||||
<div className="w-full">
|
||||
<TextInput type="password" label="Password" />
|
||||
</div>
|
||||
<div className="flex w-full flex-row items-center justify-between">
|
||||
<Button
|
||||
onClick={() => router.back()}
|
||||
variant={ButtonVariants.Secondary}
|
||||
>
|
||||
back
|
||||
</Button>
|
||||
<Button variant={ButtonVariants.Primary}>continue</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
import { getBrandingSettings } from "@/lib/zitadel";
|
||||
import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel";
|
||||
import Alert from "@/ui/Alert";
|
||||
import DynamicTheme from "@/ui/DynamicTheme";
|
||||
import VerifyEmailForm from "@/ui/VerifyEmailForm";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export default async function Page({ searchParams }: { searchParams: any }) {
|
||||
const { userId, sessionId, code, submit, organization, authRequestId } =
|
||||
searchParams;
|
||||
const {
|
||||
userId,
|
||||
loginName,
|
||||
sessionId,
|
||||
code,
|
||||
submit,
|
||||
organization,
|
||||
authRequestId,
|
||||
} = searchParams;
|
||||
|
||||
const branding = await getBrandingSettings(organization);
|
||||
|
||||
const loginSettings = await getLoginSettings(organization);
|
||||
|
||||
return (
|
||||
<DynamicTheme branding={branding}>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
@@ -17,14 +27,25 @@ export default async function Page({ searchParams }: { searchParams: any }) {
|
||||
Enter the Code provided in the verification email.
|
||||
</p>
|
||||
|
||||
{!userId && (
|
||||
<div className="py-4">
|
||||
<Alert>
|
||||
Could not get the context of the user. Make sure to provide a
|
||||
userId as searchParam.
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userId ? (
|
||||
<VerifyEmailForm
|
||||
userId={userId}
|
||||
loginName={loginName}
|
||||
code={code}
|
||||
submit={submit === "true"}
|
||||
organization={organization}
|
||||
authRequestId={authRequestId}
|
||||
sessionId={sessionId}
|
||||
loginSettings={loginSettings}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full flex flex-row items-center justify-center border border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200 rounded-md py-2 scroll-px-40">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from "@zitadel/client";
|
||||
import { IDPType } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
|
||||
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||
import {
|
||||
@@ -11,12 +12,22 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
|
||||
switch (idpType) {
|
||||
case IdentityProviderType.GITHUB:
|
||||
return "github";
|
||||
case IdentityProviderType.GITHUB_ES:
|
||||
return "github_es";
|
||||
case IdentityProviderType.GITLAB:
|
||||
return "gitlab";
|
||||
case IdentityProviderType.GITLAB_SELF_HOSTED:
|
||||
return "gitlab_es";
|
||||
case IdentityProviderType.APPLE:
|
||||
return "apple";
|
||||
case IdentityProviderType.GOOGLE:
|
||||
return "google";
|
||||
case IdentityProviderType.AZURE_AD:
|
||||
return "azure";
|
||||
case IdentityProviderType.SAML:
|
||||
return "saml";
|
||||
case IdentityProviderType.OAUTH:
|
||||
return "oauth";
|
||||
case IdentityProviderType.OIDC:
|
||||
return "oidc";
|
||||
default:
|
||||
@@ -24,6 +35,45 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this is ugly but needed atm as the getIDPByID returns a IDPType and not a IdentityProviderType
|
||||
export function idpTypeToIdentityProviderType(
|
||||
idpType: IDPType,
|
||||
): IdentityProviderType {
|
||||
switch (idpType) {
|
||||
case IDPType.IDP_TYPE_GITHUB:
|
||||
return IdentityProviderType.GITHUB;
|
||||
|
||||
case IDPType.IDP_TYPE_GITHUB_ES:
|
||||
return IdentityProviderType.GITHUB_ES;
|
||||
|
||||
case IDPType.IDP_TYPE_GITLAB:
|
||||
return IdentityProviderType.GITLAB;
|
||||
|
||||
case IDPType.IDP_TYPE_GITLAB_SELF_HOSTED:
|
||||
return IdentityProviderType.GITLAB_SELF_HOSTED;
|
||||
|
||||
case IDPType.IDP_TYPE_APPLE:
|
||||
return IdentityProviderType.APPLE;
|
||||
|
||||
case IDPType.IDP_TYPE_GOOGLE:
|
||||
return IdentityProviderType.GOOGLE;
|
||||
|
||||
case IDPType.IDP_TYPE_AZURE_AD:
|
||||
return IdentityProviderType.AZURE_AD;
|
||||
|
||||
case IDPType.IDP_TYPE_SAML:
|
||||
return IdentityProviderType.SAML;
|
||||
|
||||
case IDPType.IDP_TYPE_OAUTH:
|
||||
return IdentityProviderType.OAUTH;
|
||||
|
||||
case IDPType.IDP_TYPE_OIDC:
|
||||
return IdentityProviderType.OIDC;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown identity provider type");
|
||||
}
|
||||
}
|
||||
// this maps the IDPInformation to the AddHumanUserRequest which is used when creating a user or linking a user (email)
|
||||
// TODO: extend this object from a other file which can be overwritten by customers like map = { ...PROVIDER_MAPPING, ...customerMap }
|
||||
export type OIDC_USER = {
|
||||
@@ -35,10 +85,89 @@ export type OIDC_USER = {
|
||||
};
|
||||
};
|
||||
|
||||
const GITLAB_MAPPING = (idp: IDPInformation) => {
|
||||
const rawInfo = idp.rawInformation as {
|
||||
name: string;
|
||||
email: string;
|
||||
email_verified: boolean;
|
||||
};
|
||||
|
||||
return create(AddHumanUserRequestSchema, {
|
||||
username: idp.userName,
|
||||
email: {
|
||||
email: rawInfo.email,
|
||||
verification: { case: "isVerified", value: rawInfo.email_verified },
|
||||
},
|
||||
profile: {
|
||||
displayName: rawInfo.name || idp.userName || "",
|
||||
givenName: "",
|
||||
familyName: "",
|
||||
},
|
||||
idpLinks: [
|
||||
{
|
||||
idpId: idp.idpId,
|
||||
userId: idp.userId,
|
||||
userName: idp.userName,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const OIDC_MAPPING = (idp: IDPInformation) => {
|
||||
const rawInfo = idp.rawInformation as OIDC_USER;
|
||||
|
||||
return create(AddHumanUserRequestSchema, {
|
||||
username: idp.userName,
|
||||
email: {
|
||||
email: rawInfo.User?.email,
|
||||
verification: { case: "isVerified", value: true },
|
||||
},
|
||||
profile: {
|
||||
displayName: rawInfo.User?.name ?? "",
|
||||
givenName: rawInfo.User?.given_name ?? "",
|
||||
familyName: rawInfo.User?.family_name ?? "",
|
||||
},
|
||||
idpLinks: [
|
||||
{
|
||||
idpId: idp.idpId,
|
||||
userId: idp.userId,
|
||||
userName: idp.userName,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const GITHUB_MAPPING = (idp: IDPInformation) => {
|
||||
const rawInfo = idp.rawInformation as {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
return create(AddHumanUserRequestSchema, {
|
||||
username: idp.userName,
|
||||
email: {
|
||||
email: rawInfo.email,
|
||||
verification: { case: "isVerified", value: true },
|
||||
},
|
||||
profile: {
|
||||
displayName: rawInfo.name ?? "",
|
||||
givenName: rawInfo.name ?? "",
|
||||
familyName: rawInfo.name ?? "",
|
||||
},
|
||||
idpLinks: [
|
||||
{
|
||||
idpId: idp.idpId,
|
||||
userId: idp.userId,
|
||||
userName: idp.userName,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export const PROVIDER_MAPPING: {
|
||||
[provider: string]: (rI: IDPInformation) => AddHumanUserRequest;
|
||||
[provider: number]: (rI: IDPInformation) => AddHumanUserRequest;
|
||||
} = {
|
||||
[idpTypeToSlug(IdentityProviderType.GOOGLE)]: (idp: IDPInformation) => {
|
||||
[IdentityProviderType.GOOGLE]: (idp: IDPInformation) => {
|
||||
const rawInfo = idp.rawInformation as OIDC_USER;
|
||||
console.log(rawInfo);
|
||||
|
||||
@@ -62,7 +191,12 @@ export const PROVIDER_MAPPING: {
|
||||
],
|
||||
});
|
||||
},
|
||||
[idpTypeToSlug(IdentityProviderType.AZURE_AD)]: (idp: IDPInformation) => {
|
||||
[IdentityProviderType.GITLAB]: GITLAB_MAPPING,
|
||||
[IdentityProviderType.GITLAB_SELF_HOSTED]: GITLAB_MAPPING,
|
||||
[IdentityProviderType.OIDC]: OIDC_MAPPING,
|
||||
// check
|
||||
[IdentityProviderType.OAUTH]: OIDC_MAPPING,
|
||||
[IdentityProviderType.AZURE_AD]: (idp: IDPInformation) => {
|
||||
const rawInfo = idp.rawInformation as {
|
||||
jobTitle: string;
|
||||
mail: string;
|
||||
@@ -76,8 +210,6 @@ export const PROVIDER_MAPPING: {
|
||||
userPrincipalName: string;
|
||||
};
|
||||
|
||||
console.log(rawInfo, rawInfo.userPrincipalName);
|
||||
|
||||
return create(AddHumanUserRequestSchema, {
|
||||
username: idp.userName,
|
||||
email: {
|
||||
@@ -98,22 +230,26 @@ export const PROVIDER_MAPPING: {
|
||||
],
|
||||
});
|
||||
},
|
||||
[idpTypeToSlug(IdentityProviderType.GITHUB)]: (idp: IDPInformation) => {
|
||||
[IdentityProviderType.GITHUB]: GITHUB_MAPPING,
|
||||
[IdentityProviderType.GITHUB_ES]: GITHUB_MAPPING,
|
||||
[IdentityProviderType.APPLE]: (idp: IDPInformation) => {
|
||||
const rawInfo = idp.rawInformation as {
|
||||
email: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
return create(AddHumanUserRequestSchema, {
|
||||
username: idp.userName,
|
||||
email: {
|
||||
email: rawInfo.email,
|
||||
email: rawInfo.email ?? "",
|
||||
verification: { case: "isVerified", value: true },
|
||||
},
|
||||
profile: {
|
||||
displayName: rawInfo.name ?? "",
|
||||
givenName: rawInfo.name ?? "",
|
||||
familyName: rawInfo.name ?? "",
|
||||
givenName: rawInfo.firstName ?? "",
|
||||
familyName: rawInfo.lastName ?? "",
|
||||
},
|
||||
idpLinks: [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { createSessionForUserIdAndUpdateCookie } from "../../utils/session";
|
||||
@@ -24,6 +26,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
organizationId: command.organization,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings(command.organization);
|
||||
|
||||
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
||||
const userId = users.result[0].userId;
|
||||
const session = await createSessionForUserIdAndUpdateCookie(
|
||||
@@ -41,14 +45,102 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
session.factors?.user?.id,
|
||||
);
|
||||
|
||||
return {
|
||||
authMethodTypes: methods.authMethodTypes,
|
||||
sessionId: session.id,
|
||||
factors: session.factors,
|
||||
};
|
||||
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
||||
throw Error(
|
||||
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
loginSettings?.passkeysType &&
|
||||
loginSettings?.passkeysType === PasskeysType.ALLOWED
|
||||
) {
|
||||
paramsPassword.promptPasswordless = `true`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return redirect(
|
||||
"/passkey/login?" + new URLSearchParams(paramsPasskey),
|
||||
);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
return redirect("/passkey/login?" + new URLSearchParams(passkeyParams));
|
||||
} else if (
|
||||
methods.authMethodTypes.includes(AuthenticationMethodType.IDP)
|
||||
) {
|
||||
// TODO: redirect user to idp
|
||||
} 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) {
|
||||
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings(command.organization);
|
||||
// TODO: check if allowDomainDiscovery has to be allowed too, to redirect to the register page
|
||||
// user not found, check if register is enabled on organization
|
||||
|
||||
@@ -76,7 +168,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
params.set("organization", command.organization);
|
||||
}
|
||||
|
||||
return startIdentityProviderFlow({
|
||||
const resp = await startIdentityProviderFlow({
|
||||
idpId: identityProviders[0].id,
|
||||
urls: {
|
||||
successUrl:
|
||||
@@ -84,14 +176,14 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
failureUrl:
|
||||
`${host}/idp/${provider}/failure?` + new URLSearchParams(params),
|
||||
},
|
||||
}).then((resp: any) => {
|
||||
if (resp.authUrl) {
|
||||
return redirect(resp.authUrl);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw Error("Could not find user");
|
||||
|
||||
if (resp?.nextStep.case === "authUrl") {
|
||||
return redirect(resp.nextStep.value);
|
||||
}
|
||||
}
|
||||
|
||||
throw Error("Could not find user");
|
||||
} else if (
|
||||
loginSettings?.allowRegister &&
|
||||
loginSettings?.allowUsernamePassword
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { create } from "@zitadel/client";
|
||||
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||
import {
|
||||
SearchQuery,
|
||||
@@ -30,8 +31,6 @@ import { PROVIDER_MAPPING } from "./idp";
|
||||
|
||||
const SESSION_LIFETIME_S = 3600;
|
||||
|
||||
console.log("Session lifetime", SESSION_LIFETIME_S);
|
||||
|
||||
const transport = createServerTransport(
|
||||
process.env.ZITADEL_SERVICE_USER_TOKEN!,
|
||||
{
|
||||
@@ -438,7 +437,10 @@ export function addIDPLink(
|
||||
);
|
||||
}
|
||||
|
||||
export function createUser(provider: string, info: IDPInformation) {
|
||||
export function createUser(
|
||||
provider: IdentityProviderType,
|
||||
info: IDPInformation,
|
||||
) {
|
||||
const userData = PROVIDER_MAPPING[provider](info);
|
||||
console.log("ud", userData);
|
||||
return userService.addHumanUser(userData, {});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { FieldValues, useForm } from "react-hook-form";
|
||||
import Alert from "./Alert";
|
||||
import BackButton from "./BackButton";
|
||||
import { Button, ButtonVariants } from "./Button";
|
||||
import { TextInput } from "./Input";
|
||||
import PasswordComplexity from "./PasswordComplexity";
|
||||
@@ -55,13 +56,13 @@ export default function ChangePasswordForm({
|
||||
userId: userId,
|
||||
password: values.password,
|
||||
}).catch((error: Error) => {
|
||||
setError(error.message ?? "Could not register user");
|
||||
setError(error.message ?? "Could not change password");
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
if (!response) {
|
||||
setError("Could not register user");
|
||||
setError("Could not change password");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,9 +130,7 @@ export default function ChangePasswordForm({
|
||||
{error && <Alert>{error}</Alert>}
|
||||
|
||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||
<Button type="button" variant={ButtonVariants.Secondary}>
|
||||
back
|
||||
</Button>
|
||||
<BackButton />
|
||||
<Button
|
||||
type="submit"
|
||||
variant={ButtonVariants.Primary}
|
||||
|
||||
@@ -89,9 +89,7 @@ export default function RegisterFormWithoutPassword({
|
||||
if (withPassword) {
|
||||
return router.push(`/register?` + new URLSearchParams(registerParams));
|
||||
} else {
|
||||
const session = await submitAndRegister(value).catch((error) => {
|
||||
setError(error.message ?? "Could not register user");
|
||||
});
|
||||
const session = await submitAndRegister(value);
|
||||
|
||||
const params = new URLSearchParams({});
|
||||
if (session?.factors?.user?.loginName) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { FieldValues, useForm } from "react-hook-form";
|
||||
import Alert from "./Alert";
|
||||
import BackButton from "./BackButton";
|
||||
import { Button, ButtonVariants } from "./Button";
|
||||
import { TextInput } from "./Input";
|
||||
import PasswordComplexity from "./PasswordComplexity";
|
||||
@@ -74,19 +75,23 @@ export default function SetPasswordForm({
|
||||
setError("Could not register user");
|
||||
return;
|
||||
}
|
||||
const params: any = { userId: response.userId };
|
||||
|
||||
const params = new URLSearchParams({ userId: response.userId });
|
||||
|
||||
if (response.factors?.user?.loginName) {
|
||||
params.append("loginName", response.factors.user.loginName);
|
||||
}
|
||||
if (authRequestId) {
|
||||
params.authRequestId = authRequestId;
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
if (organization) {
|
||||
params.organization = organization;
|
||||
params.append("organization", organization);
|
||||
}
|
||||
if (response && response.sessionId) {
|
||||
params.sessionId = response.sessionId;
|
||||
params.append("sessionId", response.sessionId);
|
||||
}
|
||||
|
||||
return router.push(`/verify?` + new URLSearchParams(params));
|
||||
return router.push(`/verify?` + params);
|
||||
}
|
||||
|
||||
const { errors } = formState;
|
||||
@@ -150,9 +155,7 @@ export default function SetPasswordForm({
|
||||
{error && <Alert>{error}</Alert>}
|
||||
|
||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
||||
<Button type="button" variant={ButtonVariants.Secondary}>
|
||||
back
|
||||
</Button>
|
||||
<BackButton />
|
||||
<Button
|
||||
type="submit"
|
||||
variant={ButtonVariants.Primary}
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ReactNode, useState } from "react";
|
||||
import Alert from "./Alert";
|
||||
import { SignInWithApple } from "./idps/SignInWithApple";
|
||||
import { SignInWithAzureAD } from "./idps/SignInWithAzureAD";
|
||||
import { SignInWithGeneric } from "./idps/SignInWithGeneric";
|
||||
import { SignInWithGithub } from "./idps/SignInWithGithub";
|
||||
import { SignInWithGitlab } from "./idps/SignInWithGitlab";
|
||||
import { SignInWithGoogle } from "./idps/SignInWithGoogle";
|
||||
@@ -76,10 +78,41 @@ export function SignInWithIDP({
|
||||
{identityProviders &&
|
||||
identityProviders.map((idp, i) => {
|
||||
switch (idp.type) {
|
||||
case IdentityProviderType.APPLE:
|
||||
return (
|
||||
<SignInWithApple
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.APPLE)
|
||||
}
|
||||
></SignInWithApple>
|
||||
);
|
||||
case IdentityProviderType.OAUTH:
|
||||
return (
|
||||
<SignInWithGeneric
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.OAUTH)
|
||||
}
|
||||
></SignInWithGeneric>
|
||||
);
|
||||
case IdentityProviderType.OIDC:
|
||||
return (
|
||||
<SignInWithGeneric
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.OIDC)
|
||||
}
|
||||
></SignInWithGeneric>
|
||||
);
|
||||
case IdentityProviderType.GITHUB:
|
||||
return (
|
||||
<SignInWithGithub
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB)
|
||||
}
|
||||
@@ -89,6 +122,7 @@ export function SignInWithIDP({
|
||||
return (
|
||||
<SignInWithGithub
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.GITHUB_ES)
|
||||
}
|
||||
@@ -98,6 +132,7 @@ export function SignInWithIDP({
|
||||
return (
|
||||
<SignInWithAzureAD
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.AZURE_AD)
|
||||
}
|
||||
@@ -118,6 +153,7 @@ export function SignInWithIDP({
|
||||
return (
|
||||
<SignInWithGitlab
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(idp.id, IdentityProviderType.GITLAB)
|
||||
}
|
||||
@@ -127,6 +163,7 @@ export function SignInWithIDP({
|
||||
return (
|
||||
<SignInWithGitlab
|
||||
key={`idp-${i}`}
|
||||
name={idp.name}
|
||||
onClick={() =>
|
||||
navigateToAuthUrl(
|
||||
idp.id,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { sendLoginname } from "@/lib/server/loginname";
|
||||
import {
|
||||
LoginSettings,
|
||||
PasskeysType,
|
||||
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -20,7 +15,6 @@ type Inputs = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
loginSettings: LoginSettings | undefined;
|
||||
loginName: string | undefined;
|
||||
authRequestId: string | undefined;
|
||||
organization?: string;
|
||||
@@ -30,7 +24,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function UsernameForm({
|
||||
loginSettings,
|
||||
loginName,
|
||||
authRequestId,
|
||||
organization,
|
||||
@@ -59,7 +52,6 @@ export default function UsernameForm({
|
||||
authRequestId,
|
||||
}).catch((error: Error) => {
|
||||
setError(error.message ?? "An internal error occurred");
|
||||
return Promise.reject(error ?? "An internal error occurred");
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
@@ -72,124 +64,6 @@ export default function UsernameForm({
|
||||
organization?: string,
|
||||
) {
|
||||
const response = await submitLoginName(values, organization);
|
||||
|
||||
if (!response) {
|
||||
setError("An internal error occurred");
|
||||
return;
|
||||
}
|
||||
|
||||
if (response?.authMethodTypes && response.authMethodTypes.length === 0) {
|
||||
setError(
|
||||
"User has no available authentication methods. Contact your administrator to setup authentication for the requested user.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response?.authMethodTypes.length == 1) {
|
||||
const method = response.authMethodTypes[0];
|
||||
switch (method) {
|
||||
case AuthenticationMethodType.PASSWORD: // user has only password as auth method
|
||||
const paramsPassword: any = {
|
||||
loginName: response?.factors?.user?.loginName,
|
||||
};
|
||||
|
||||
// TODO: does this have to be checked in loginSettings.allowDomainDiscovery
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPassword.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
if (
|
||||
loginSettings?.passkeysType &&
|
||||
loginSettings?.passkeysType === PasskeysType.ALLOWED
|
||||
) {
|
||||
paramsPassword.promptPasswordless = `true`;
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPassword.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPassword),
|
||||
);
|
||||
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||
const paramsPasskey: any = { loginName: values.loginName };
|
||||
if (authRequestId) {
|
||||
paramsPasskey.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPasskey.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" + new URLSearchParams(paramsPasskey),
|
||||
);
|
||||
default:
|
||||
const paramsPasskeyDefault: any = { loginName: values.loginName };
|
||||
|
||||
if (loginSettings?.passkeysType === 1) {
|
||||
paramsPasskeyDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPasskeyDefault.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPasskeyDefault.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPasskeyDefault),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// prefer passkey in favor of other methods
|
||||
if (response?.authMethodTypes.includes(2)) {
|
||||
const passkeyParams: any = {
|
||||
loginName: values.loginName,
|
||||
altPassword: `${response.authMethodTypes.includes(1)}`, // show alternative password option
|
||||
};
|
||||
|
||||
if (authRequestId) {
|
||||
passkeyParams.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
passkeyParams.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/passkey/login?" + new URLSearchParams(passkeyParams),
|
||||
);
|
||||
} else {
|
||||
// user has no passkey setup and login settings allow passkeys
|
||||
const paramsPasswordDefault: any = { loginName: values.loginName };
|
||||
|
||||
if (loginSettings?.passkeysType === 1) {
|
||||
paramsPasswordDefault.promptPasswordless = `true`; // PasskeysType.PASSKEYS_TYPE_ALLOWED,
|
||||
}
|
||||
|
||||
if (authRequestId) {
|
||||
paramsPasswordDefault.authRequestId = authRequestId;
|
||||
}
|
||||
|
||||
if (organization || response?.factors?.user?.organizationId) {
|
||||
paramsPasswordDefault.organization =
|
||||
organization ?? response?.factors?.user?.organizationId;
|
||||
}
|
||||
|
||||
return router.push(
|
||||
"/password?" + new URLSearchParams(paramsPasswordDefault),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { resendVerifyEmail, verifyUserByEmail } from "@/lib/server/email";
|
||||
import Alert from "@/ui/Alert";
|
||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -15,20 +16,24 @@ type Inputs = {
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
loginName: string;
|
||||
code: string;
|
||||
submit: boolean;
|
||||
organization?: string;
|
||||
authRequestId?: string;
|
||||
sessionId?: string;
|
||||
loginSettings?: LoginSettings;
|
||||
};
|
||||
|
||||
export default function VerifyEmailForm({
|
||||
userId,
|
||||
loginName,
|
||||
code,
|
||||
submit,
|
||||
organization,
|
||||
authRequestId,
|
||||
sessionId,
|
||||
loginSettings,
|
||||
}: Props) {
|
||||
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||
mode: "onBlur",
|
||||
@@ -71,7 +76,7 @@ export default function VerifyEmailForm({
|
||||
userId,
|
||||
}).catch((error: Error) => {
|
||||
setLoading(false);
|
||||
setError(error.message);
|
||||
setError(error.message ?? "Could not verify email");
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
|
||||
34
apps/login/src/ui/idps/SignInWithApple.tsx
Normal file
34
apps/login/src/ui/idps/SignInWithApple.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, forwardRef } from "react";
|
||||
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
|
||||
|
||||
export const SignInWithApple = forwardRef<
|
||||
HTMLButtonElement,
|
||||
SignInWithIdentityProviderProps
|
||||
>(
|
||||
({ children, className = "", name = "", ...props }, ref): ReactNode => (
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
className={`${IdpButtonClasses} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
<div className="h-12 w-12 flex items-center justify-center">
|
||||
<div className="h-6 w-6">
|
||||
<svg viewBox="0 0 170 170" fill="currentColor">
|
||||
<title>Apple Logo</title>
|
||||
<path d="M150.37 130.25c-2.45 5.66-5.35 10.87-8.71 15.66-4.58 6.53-8.33 11.05-11.22 13.56-4.48 4.12-9.28 6.23-14.42 6.35-3.69 0-8.14-1.05-13.32-3.18-5.197-2.12-9.973-3.17-14.34-3.17-4.58 0-9.492 1.05-14.746 3.17-5.262 2.13-9.501 3.24-12.742 3.35-4.929.21-9.842-1.96-14.746-6.52-3.13-2.73-7.045-7.41-11.735-14.04-5.032-7.08-9.169-15.29-12.41-24.65-3.471-10.11-5.211-19.9-5.211-29.378 0-10.857 2.346-20.221 7.045-28.068 3.693-6.303 8.606-11.275 14.755-14.925s12.793-5.51 19.948-5.629c3.915 0 9.049 1.211 15.429 3.591 6.362 2.388 10.447 3.599 12.238 3.599 1.339 0 5.877-1.416 13.57-4.239 7.275-2.618 13.415-3.702 18.445-3.275 13.63 1.1 23.87 6.473 30.68 16.153-12.19 7.386-18.22 17.731-18.1 31.002.11 10.337 3.86 18.939 11.23 25.769 3.34 3.17 7.07 5.62 11.22 7.36-.9 2.61-1.85 5.11-2.86 7.51zM119.11 7.24c0 8.102-2.96 15.667-8.86 22.669-7.12 8.324-15.732 13.134-25.071 12.375a25.222 25.222 0 0 1-.188-3.07c0-7.778 3.386-16.102 9.399-22.908 3.002-3.446 6.82-6.311 11.45-8.597 4.62-2.252 8.99-3.497 13.1-3.71.12 1.083.17 2.166.17 3.24z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<span className="ml-4">{name ? name : "Sign in with Apple"}</span>
|
||||
)}
|
||||
</button>
|
||||
),
|
||||
);
|
||||
|
||||
SignInWithApple.displayName = "SignInWithApple";
|
||||
25
apps/login/src/ui/idps/SignInWithGeneric.tsx
Normal file
25
apps/login/src/ui/idps/SignInWithGeneric.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, forwardRef } from "react";
|
||||
import { IdpButtonClasses, SignInWithIdentityProviderProps } from "./classes";
|
||||
|
||||
export const SignInWithGeneric = forwardRef<
|
||||
HTMLButtonElement,
|
||||
SignInWithIdentityProviderProps
|
||||
>(
|
||||
(
|
||||
{ children, className = "h-[50px] pl-20", name = "", ...props },
|
||||
ref,
|
||||
): ReactNode => (
|
||||
<button
|
||||
type="button"
|
||||
ref={ref}
|
||||
className={`${IdpButtonClasses} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children ? children : <span className="">{name}</span>}
|
||||
</button>
|
||||
),
|
||||
);
|
||||
|
||||
SignInWithGeneric.displayName = "SignInWithGeneric";
|
||||
@@ -3,5 +3,5 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors";
|
||||
|
||||
// TODO: Move this to `./protobuf.ts` and export it from there
|
||||
export { create, fromJson, toJson } from "@bufbuild/protobuf";
|
||||
export { TimestampSchema, timestampDate } from "@bufbuild/protobuf/wkt";
|
||||
export { TimestampSchema, timestampDate, timestampFromDate } from "@bufbuild/protobuf/wkt";
|
||||
export type { Timestamp } from "@bufbuild/protobuf/wkt";
|
||||
|
||||
9629
pnpm-lock.yaml
generated
9629
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user