idp for invites

This commit is contained in:
Max Peintner
2024-11-29 13:38:56 +01:00
parent 837f45b36c
commit bac941890a
5 changed files with 185 additions and 145 deletions

View File

@@ -2,10 +2,12 @@ import { Alert } from "@/components/alert";
import { BackButton } from "@/components/back-button"; import { BackButton } from "@/components/back-button";
import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,
getLoginSettings, getLoginSettings,
getSession, getSession,
@@ -74,6 +76,10 @@ export default async function Page(props: {
}); });
} }
if (!sessionWithData) {
return <Alert>{tError("unknownContext")}</Alert>;
}
const branding = await getBrandingSettings( const branding = await getBrandingSettings(
sessionWithData.factors?.user?.organizationId, sessionWithData.factors?.user?.organizationId,
); );
@@ -82,22 +88,32 @@ export default async function Page(props: {
sessionWithData.factors?.user?.organizationId, sessionWithData.factors?.user?.organizationId,
); );
const identityProviders = await getActiveIdentityProviders(
sessionWithData.factors?.user?.organizationId,
).then((resp) => {
return resp.identityProviders;
});
const params = new URLSearchParams({ const params = new URLSearchParams({
initial: "true", // defines that a code is not required and is therefore not shown in the UI initial: "true", // defines that a code is not required and is therefore not shown in the UI
}); });
if (loginName) { if (sessionWithData.factors?.user?.loginName) {
params.set("loginName", loginName); params.set("loginName", sessionWithData.factors?.user?.loginName);
} }
if (organization) { if (sessionWithData.factors?.user?.organizationId) {
params.set("organization", organization); params.set("organization", sessionWithData.factors?.user?.organizationId);
} }
if (authRequestId) { if (authRequestId) {
params.set("authRequestId", authRequestId); params.set("authRequestId", authRequestId);
} }
const host = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
return ( return (
<DynamicTheme branding={branding}> <DynamicTheme branding={branding}>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
@@ -105,18 +121,14 @@ export default async function Page(props: {
<p className="ztdl-p">{t("description")}</p> <p className="ztdl-p">{t("description")}</p>
{sessionWithData && ( <UserAvatar
<UserAvatar loginName={sessionWithData.factors?.user?.loginName}
loginName={loginName ?? sessionWithData.factors?.user?.loginName} displayName={sessionWithData.factors?.user?.displayName}
displayName={sessionWithData.factors?.user?.displayName} showDropdown
showDropdown searchParams={searchParams}
searchParams={searchParams} ></UserAvatar>
></UserAvatar>
)}
{!(loginName || sessionId) && <Alert>{tError("unknownContext")}</Alert>} {loginSettings && (
{loginSettings && sessionWithData && (
<ChooseAuthenticatorToSetup <ChooseAuthenticatorToSetup
authMethods={sessionWithData.authMethods} authMethods={sessionWithData.authMethods}
loginSettings={loginSettings} loginSettings={loginSettings}
@@ -124,6 +136,20 @@ export default async function Page(props: {
></ChooseAuthenticatorToSetup> ></ChooseAuthenticatorToSetup>
)} )}
<p className="ztdl-p text-center">
or sign in with an Identity Provider
</p>
{loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp
host={host}
identityProviders={identityProviders}
authRequestId={authRequestId}
organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp>
)}
<div className="mt-8 flex w-full flex-row items-center"> <div className="mt-8 flex w-full flex-row items-center">
<BackButton /> <BackButton />
<span className="flex-grow"></span> <span className="flex-grow"></span>

View File

@@ -37,7 +37,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const locale = getLocale(); const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" }); const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, authRequestId, organization } = searchParams; const { id, token, authRequestId, organization, link } = searchParams;
const { provider } = params; const { provider } = params;
const branding = await getBrandingSettings(organization); const branding = await getBrandingSettings(organization);
@@ -50,7 +50,8 @@ export default async function Page(props: {
const { idpInformation, userId } = intent; const { idpInformation, userId } = intent;
if (userId) { // sign in user. If user should be linked continue
if (userId && !link) {
// TODO: update user if idp.options.isAutoUpdate is true // TODO: update user if idp.options.isAutoUpdate is true
return ( return (

View File

@@ -2,23 +2,14 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UsernameForm } from "@/components/username-form"; import { UsernameForm } from "@/components/username-form";
import { import {
getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,
getLoginSettings, getLoginSettings,
settingsService,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { makeReqCtx } from "@zitadel/client/v2";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
function getIdentityProviders(orgId?: string) {
return settingsService
.getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {})
.then((resp) => {
return resp.identityProviders;
});
}
export default async function Page(props: { export default async function Page(props: {
searchParams: Promise<Record<string | number | symbol, string | undefined>>; searchParams: Promise<Record<string | number | symbol, string | undefined>>;
}) { }) {
@@ -47,9 +38,11 @@ export default async function Page(props: {
organization ?? defaultOrganization, organization ?? defaultOrganization,
); );
const identityProviders = await getIdentityProviders( const identityProviders = await getActiveIdentityProviders(
organization ?? defaultOrganization, organization ?? defaultOrganization,
); ).then((resp) => {
return resp.identityProviders;
});
const branding = await getBrandingSettings( const branding = await getBrandingSettings(
organization ?? defaultOrganization, organization ?? defaultOrganization,
@@ -68,7 +61,7 @@ export default async function Page(props: {
submit={submit} submit={submit}
allowRegister={!!loginSettings?.allowRegister} allowRegister={!!loginSettings?.allowRegister}
> >
{identityProviders && process.env.ZITADEL_API_URL && ( {identityProviders && (
<SignInWithIdp <SignInWithIdp
host={host} host={host}
identityProviders={identityProviders} identityProviders={identityProviders}

View File

@@ -22,6 +22,7 @@ export interface SignInWithIDPProps {
identityProviders: IdentityProvider[]; identityProviders: IdentityProvider[];
authRequestId?: string; authRequestId?: string;
organization?: string; organization?: string;
linkOnly?: boolean;
} }
export function SignInWithIdp({ export function SignInWithIdp({
@@ -29,6 +30,7 @@ export function SignInWithIdp({
identityProviders, identityProviders,
authRequestId, authRequestId,
organization, organization,
linkOnly,
}: SignInWithIDPProps) { }: SignInWithIDPProps) {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@@ -39,6 +41,10 @@ export function SignInWithIdp({
const params = new URLSearchParams(); const params = new URLSearchParams();
if (linkOnly) {
params.set("link", "true");
}
if (authRequestId) { if (authRequestId) {
params.set("authRequestId", authRequestId); params.set("authRequestId", authRequestId);
} }
@@ -70,121 +76,134 @@ export function SignInWithIdp({
return ( return (
<div className="flex flex-col w-full space-y-2 text-sm"> <div className="flex flex-col w-full space-y-2 text-sm">
{identityProviders && {identityProviders &&
identityProviders.map((idp, i) => { identityProviders
switch (idp.type) { // .filter((idp) =>
case IdentityProviderType.APPLE: // linkOnly ? idp.config?.options.isLinkingAllowed : true,
return ( // )
<SignInWithApple .map((idp, i) => {
key={`idp-${i}`} switch (idp.type) {
name={idp.name} case IdentityProviderType.APPLE:
onClick={() => return (
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.APPLE)) <SignInWithApple
} key={`idp-${i}`}
></SignInWithApple> name={idp.name}
); onClick={() =>
case IdentityProviderType.OAUTH: startFlow(
return ( idp.id,
<SignInWithGeneric idpTypeToSlug(IdentityProviderType.APPLE),
key={`idp-${i}`} )
name={idp.name} }
onClick={() => ></SignInWithApple>
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OAUTH)) );
} case IdentityProviderType.OAUTH:
></SignInWithGeneric> return (
); <SignInWithGeneric
case IdentityProviderType.OIDC: key={`idp-${i}`}
return ( name={idp.name}
<SignInWithGeneric onClick={() =>
key={`idp-${i}`} startFlow(
name={idp.name} idp.id,
onClick={() => idpTypeToSlug(IdentityProviderType.OAUTH),
startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OIDC)) )
} }
></SignInWithGeneric> ></SignInWithGeneric>
); );
case IdentityProviderType.GITHUB: case IdentityProviderType.OIDC:
return ( return (
<SignInWithGithub <SignInWithGeneric
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow( startFlow(
idp.id, idp.id,
idpTypeToSlug(IdentityProviderType.GITHUB), idpTypeToSlug(IdentityProviderType.OIDC),
) )
} }
></SignInWithGithub> ></SignInWithGeneric>
); );
case IdentityProviderType.GITHUB_ES: case IdentityProviderType.GITHUB:
return ( return (
<SignInWithGithub <SignInWithGithub
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow( startFlow(
idp.id, idp.id,
idpTypeToSlug(IdentityProviderType.GITHUB_ES), idpTypeToSlug(IdentityProviderType.GITHUB),
) )
} }
></SignInWithGithub> ></SignInWithGithub>
); );
case IdentityProviderType.AZURE_AD: case IdentityProviderType.GITHUB_ES:
return ( return (
<SignInWithAzureAd <SignInWithGithub
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow( startFlow(
idp.id, idp.id,
idpTypeToSlug(IdentityProviderType.AZURE_AD), idpTypeToSlug(IdentityProviderType.GITHUB_ES),
) )
} }
></SignInWithAzureAd> ></SignInWithGithub>
); );
case IdentityProviderType.GOOGLE: case IdentityProviderType.AZURE_AD:
return ( return (
<SignInWithGoogle <SignInWithAzureAd
key={`idp-${i}`} key={`idp-${i}`}
e2e="google" name={idp.name}
name={idp.name} onClick={() =>
onClick={() => startFlow(
startFlow( idp.id,
idp.id, idpTypeToSlug(IdentityProviderType.AZURE_AD),
idpTypeToSlug(IdentityProviderType.GOOGLE), )
) }
} ></SignInWithAzureAd>
></SignInWithGoogle> );
); case IdentityProviderType.GOOGLE:
case IdentityProviderType.GITLAB: return (
return ( <SignInWithGoogle
<SignInWithGitlab key={`idp-${i}`}
key={`idp-${i}`} e2e="google"
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow( startFlow(
idp.id, idp.id,
idpTypeToSlug(IdentityProviderType.GITLAB), idpTypeToSlug(IdentityProviderType.GOOGLE),
) )
} }
></SignInWithGitlab> ></SignInWithGoogle>
); );
case IdentityProviderType.GITLAB_SELF_HOSTED: case IdentityProviderType.GITLAB:
return ( return (
<SignInWithGitlab <SignInWithGitlab
key={`idp-${i}`} key={`idp-${i}`}
name={idp.name} name={idp.name}
onClick={() => onClick={() =>
startFlow( startFlow(
idp.id, idp.id,
idpTypeToSlug(IdentityProviderType.GITLAB_SELF_HOSTED), idpTypeToSlug(IdentityProviderType.GITLAB),
) )
} }
></SignInWithGitlab> ></SignInWithGitlab>
); );
default: case IdentityProviderType.GITLAB_SELF_HOSTED:
return null; return (
} <SignInWithGitlab
})} key={`idp-${i}`}
name={idp.name}
onClick={() =>
startFlow(
idp.id,
idpTypeToSlug(IdentityProviderType.GITLAB_SELF_HOSTED),
)
}
></SignInWithGitlab>
);
default:
return null;
}
})}
{error && ( {error && (
<div className="py-4"> <div className="py-4">
<Alert>{error}</Alert> <Alert>{error}</Alert>

View File

@@ -149,6 +149,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
); );
const humanUser = user.type.case === "human" ? user.type.value : undefined; const humanUser = user.type.case === "human" ? user.type.value : undefined;
console.log("humanUser", humanUser);
if ( if (
availableSecondFactors?.length == 0 && availableSecondFactors?.length == 0 &&
humanUser?.passwordChangeRequired humanUser?.passwordChangeRequired