From 9c782dfed96971e4959e067aab8e132b73cf13c7 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 13 Dec 2024 16:31:51 +0100 Subject: [PATCH 01/18] handle session creation at the end of linking / finish OIDC flow --- .../(login)/idp/[provider]/success/page.tsx | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index b536456c..c9353a64 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,7 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; -import { IdpSignin } from "@/components/idp-signin"; import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp"; +import { createNewSessionForIdp } from "@/lib/server/session"; import { addIDPLink, createUser, @@ -13,6 +13,7 @@ import { import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { getLocale, getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; async function loginFailed(branding?: BrandingSettings) { const locale = getLocale(); @@ -50,24 +51,42 @@ export default async function Page(props: { const { idpInformation, userId } = intent; + async function continueWithSession( + idpIntentId: string, + idpIntentToken: string, + ) { + const sessionRedirectResponse = await createNewSessionForIdp({ + userId, + idpIntent: { + idpIntentId, + idpIntentToken, + }, + authRequestId, + }); + + if ( + !sessionRedirectResponse || + (sessionRedirectResponse && + "error" in sessionRedirectResponse && + sessionRedirectResponse?.error) + ) { + return loginFailed(branding); + } + + if ( + sessionRedirectResponse && + "redirect" in sessionRedirectResponse && + sessionRedirectResponse?.redirect + ) { + return redirect(sessionRedirectResponse.redirect); + } + } + // sign in user. If user should be linked continue if (userId && !link) { // TODO: update user if idp.options.isAutoUpdate is true - return ( - -
-

{t("loginSuccess.title")}

-
{t("loginSuccess.description")}
- - -
-
- ); + await continueWithSession(id, token); } if (!idpInformation) { @@ -134,17 +153,7 @@ export default async function Page(props: { ); }); - if (idpLink) { - return ( - // TODO: possibily login user now - -
-

{t("linkingSuccess.title")}

-
{t("linkingSuccess.description")}
-
-
- ); - } + await continueWithSession(id, token); } } From adb08333ed0dbbfb23afc02da62d52bc50db8b1a Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:21:12 +0100 Subject: [PATCH 02/18] continue with session --- .../(login)/idp/[provider]/success/page.tsx | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index c9353a64..0e565ae3 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -1,7 +1,7 @@ import { Alert, AlertType } from "@/components/alert"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { IdpSignin } from "@/components/idp-signin"; import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp"; -import { createNewSessionForIdp } from "@/lib/server/session"; import { addIDPLink, createUser, @@ -13,7 +13,6 @@ import { import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { getLocale, getTranslations } from "next-intl/server"; -import { redirect } from "next/navigation"; async function loginFailed(branding?: BrandingSettings) { const locale = getLocale(); @@ -51,42 +50,16 @@ export default async function Page(props: { const { idpInformation, userId } = intent; - async function continueWithSession( - idpIntentId: string, - idpIntentToken: string, - ) { - const sessionRedirectResponse = await createNewSessionForIdp({ - userId, - idpIntent: { - idpIntentId, - idpIntentToken, - }, - authRequestId, - }); - - if ( - !sessionRedirectResponse || - (sessionRedirectResponse && - "error" in sessionRedirectResponse && - sessionRedirectResponse?.error) - ) { - return loginFailed(branding); - } - - if ( - sessionRedirectResponse && - "redirect" in sessionRedirectResponse && - sessionRedirectResponse?.redirect - ) { - return redirect(sessionRedirectResponse.redirect); - } - } - // sign in user. If user should be linked continue if (userId && !link) { // TODO: update user if idp.options.isAutoUpdate is true - await continueWithSession(id, token); + return ( + + ); } if (!idpInformation) { @@ -153,7 +126,12 @@ export default async function Page(props: { ); }); - await continueWithSession(id, token); + return ( + + ); } } From 32be5140ea05827d75ac7bc5f40d8b57581a5ec7 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:24:31 +0100 Subject: [PATCH 03/18] wrap around login --- .../(login)/idp/[provider]/success/page.tsx | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 0e565ae3..c9704bf2 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -29,6 +29,28 @@ async function loginFailed(branding?: BrandingSettings) { ); } + +async function loginSuccess( + userId: string, + idpIntent: { idpIntentId: string; idpIntentToken: string }, + branding?: BrandingSettings, +) { + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "idp" }); + + return ( + +
+

{t("loginSuccess.title")}

+
+ {t("loginSuccess.description")} + +
+
+
+ ); +} + export default async function Page(props: { searchParams: Promise>; params: Promise<{ provider: string }>; @@ -54,11 +76,10 @@ export default async function Page(props: { if (userId && !link) { // TODO: update user if idp.options.isAutoUpdate is true - return ( - + return loginSuccess( + userId, + { idpIntentId: id, idpIntentToken: token }, + branding, ); } @@ -126,11 +147,10 @@ export default async function Page(props: { ); }); - return ( - + return loginSuccess( + userId, + { idpIntentId: id, idpIntentToken: token }, + branding, ); } } From d7c79c6cb8deefa68d51b774eb2a6ae611800f4a Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:33:18 +0100 Subject: [PATCH 04/18] log on server --- apps/login/src/lib/server/session.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 69aac95a..c20518c3 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -60,6 +60,8 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) { return { error: "Could not create session" }; } + console.log("sessionForIdp", session, authRequestId, session.id); + const url = await getNextUrl( authRequestId && session.id ? { From 9e8b11fb60b3bb614443c2245d610ec339b7d2d4 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:36:41 +0100 Subject: [PATCH 05/18] clean --- apps/login/src/lib/server/cookie.ts | 71 ++++++++++++++--------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index ab1513ac..91447174 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -91,47 +91,44 @@ export async function createSessionForIdpAndUpdateCookie( lifetime, ); - if (createdSession) { - return getSession({ - sessionId: createdSession.sessionId, - sessionToken: createdSession.sessionToken, - }).then((response) => { - if (response?.session && response.session?.factors?.user?.loginName) { - const sessionCookie: CustomCookieData = { - id: createdSession.sessionId, - token: createdSession.sessionToken, - creationTs: response.session.creationDate - ? `${timestampMs(response.session.creationDate)}` - : "", - expirationTs: response.session.expirationDate - ? `${timestampMs(response.session.expirationDate)}` - : "", - changeTs: response.session.changeDate - ? `${timestampMs(response.session.changeDate)}` - : "", - loginName: response.session.factors.user.loginName ?? "", - organization: response.session.factors.user.organizationId ?? "", - }; + if (!createdSession) { + throw "Could not create session"; + } - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } + const { session } = await getSession({ + sessionId: createdSession.sessionId, + sessionToken: createdSession.sessionToken, + }); - if (response.session.factors.user.organizationId) { - sessionCookie.organization = - response.session.factors.user.organizationId; - } + if (!session || !session.factors?.user?.loginName) { + throw "Could not retrieve session"; + } - return addSessionToCookie(sessionCookie).then(() => { - return response.session as Session; - }); - } else { - throw "could not get session or session does not have loginName"; - } - }); - } else { - throw "Could not create session"; + const sessionCookie: CustomCookieData = { + id: createdSession.sessionId, + token: createdSession.sessionToken, + creationTs: session.creationDate + ? `${timestampMs(session.creationDate)}` + : "", + expirationTs: session.expirationDate + ? `${timestampMs(session.expirationDate)}` + : "", + changeTs: session.changeDate ? `${timestampMs(session.changeDate)}` : "", + loginName: session.factors.user.loginName ?? "", + organization: session.factors.user.organizationId ?? "", + }; + + if (authRequestId) { + sessionCookie.authRequestId = authRequestId; + } + + if (session.factors.user.organizationId) { + sessionCookie.organization = session.factors.user.organizationId; } + + return addSessionToCookie(sessionCookie).then(() => { + return session as Session; + }); } export type SessionWithChallenges = Session & { From b99c73ff242abf3524f5ea4a8c30418a68e8aaaf Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:43:37 +0100 Subject: [PATCH 06/18] add authrequest to context --- apps/login/src/app/(login)/idp/[provider]/success/page.tsx | 7 ++++++- apps/login/src/lib/server/session.ts | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index c9704bf2..cac12496 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -33,6 +33,7 @@ async function loginFailed(branding?: BrandingSettings) { async function loginSuccess( userId: string, idpIntent: { idpIntentId: string; idpIntentToken: string }, + authRequestId?: string, branding?: BrandingSettings, ) { const locale = getLocale(); @@ -44,7 +45,11 @@ async function loginSuccess(

{t("loginSuccess.title")}

{t("loginSuccess.description")} - +
diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index c20518c3..69aac95a 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -60,8 +60,6 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) { return { error: "Could not create session" }; } - console.log("sessionForIdp", session, authRequestId, session.id); - const url = await getNextUrl( authRequestId && session.id ? { From 18d865583862014029a4444501c41c9e2f7989b6 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:43:56 +0100 Subject: [PATCH 07/18] missing params --- apps/login/src/app/(login)/idp/[provider]/success/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index cac12496..59f2bba2 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -84,6 +84,7 @@ export default async function Page(props: { return loginSuccess( userId, { idpIntentId: id, idpIntentToken: token }, + authRequestId, branding, ); } @@ -155,6 +156,7 @@ export default async function Page(props: { return loginSuccess( userId, { idpIntentId: id, idpIntentToken: token }, + authRequestId, branding, ); } From 725d03131bd3286bd56402923d308f8b69aa3ebc Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 09:58:55 +0100 Subject: [PATCH 08/18] linking success page --- .../(login)/idp/[provider]/success/page.tsx | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 59f2bba2..4f7ad163 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -56,6 +56,32 @@ async function loginSuccess( ); } +async function linkingSuccess( + userId: string, + idpIntent: { idpIntentId: string; idpIntentToken: string }, + authRequestId?: string, + branding?: BrandingSettings, +) { + const locale = getLocale(); + const t = await getTranslations({ locale, namespace: "idp" }); + + return ( + +
+

{t("linkingSuccess.title")}

+
+ {t("linkingSuccess.description")} + +
+
+
+ ); +} + export default async function Page(props: { searchParams: Promise>; params: Promise<{ provider: string }>; @@ -153,7 +179,7 @@ export default async function Page(props: { ); }); - return loginSuccess( + return linkingSuccess( userId, { idpIntentId: id, idpIntentToken: token }, authRequestId, From b93f00d124e21ff43decded1f74e89c84d135d23 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 10:00:43 +0100 Subject: [PATCH 09/18] branding context --- apps/login/src/app/(login)/idp/[provider]/success/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 4f7ad163..a90ab3e4 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -204,5 +204,5 @@ export default async function Page(props: { } // return login failed if no linking or creation is allowed and no user was found - return loginFailed; + return loginFailed(branding); } From 8c9302250e05867a45b7727d02a312bfa30f3ea1 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 10:14:38 +0100 Subject: [PATCH 10/18] linking with userid --- .../app/(login)/idp/[provider]/success/page.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index a90ab3e4..4c09790a 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -179,12 +179,14 @@ export default async function Page(props: { ); }); - return linkingSuccess( - userId, - { idpIntentId: id, idpIntentToken: token }, - authRequestId, - branding, - ); + if (idpLink) { + return linkingSuccess( + idpInformation.userId, + { idpIntentId: id, idpIntentToken: token }, + authRequestId, + branding, + ); + } } } From 495d02241b205d53f69a6a9cf12b56d215d457fc Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 10:21:00 +0100 Subject: [PATCH 11/18] change user id --- apps/login/src/app/(login)/idp/[provider]/success/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 4c09790a..b80dad2c 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -181,7 +181,7 @@ export default async function Page(props: { if (idpLink) { return linkingSuccess( - idpInformation.userId, + foundUser.userId, { idpIntentId: id, idpIntentToken: token }, authRequestId, branding, From 5860b79ab39c5cc89cad4d294f5cc2cc46a1029e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 10:30:18 +0100 Subject: [PATCH 12/18] show loading state, p styles --- .../(login)/idp/[provider]/success/page.tsx | 30 +++++++++---------- apps/login/src/components/idp-signin.tsx | 6 ++-- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index b80dad2c..58af6526 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -43,14 +43,13 @@ async function loginSuccess(

{t("loginSuccess.title")}

-
- {t("loginSuccess.description")} - -
+

{t("loginSuccess.description")}

+ +
); @@ -69,14 +68,13 @@ async function linkingSuccess(

{t("linkingSuccess.title")}

-
- {t("linkingSuccess.description")} - -
+

{t("linkingSuccess.description")}

+ +
); diff --git a/apps/login/src/components/idp-signin.tsx b/apps/login/src/components/idp-signin.tsx index 6613e7bf..543cd64b 100644 --- a/apps/login/src/components/idp-signin.tsx +++ b/apps/login/src/components/idp-signin.tsx @@ -21,7 +21,7 @@ export function IdpSignin({ idpIntent: { idpIntentId, idpIntentToken }, authRequestId, }: Props) { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const router = useRouter(); @@ -55,8 +55,8 @@ export function IdpSignin({ }, []); return ( -
- {loading && } +
+ {loading && } {error && (
{error} From 94413db9f515013a3db2dc7950c74ea654673dad Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 10:36:40 +0100 Subject: [PATCH 13/18] log error --- .../app/(login)/idp/[provider]/success/page.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 58af6526..9209f4a4 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -14,7 +14,7 @@ import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { getLocale, getTranslations } from "next-intl/server"; -async function loginFailed(branding?: BrandingSettings) { +async function loginFailed(branding?: BrandingSettings, error: string = "") { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); @@ -22,9 +22,12 @@ async function loginFailed(branding?: BrandingSettings) {

{t("loginError.title")}

-
- {{t("loginError.title")}} -
+

{t("loginError.title")}

+ {error && ( +
+ {{error}} +
+ )}
); @@ -94,7 +97,7 @@ export default async function Page(props: { const branding = await getBrandingSettings(organization); if (!provider || !id || !token) { - return loginFailed(branding); + return loginFailed(branding, "IDP context missing"); } const intent = await retrieveIDPIntent(id, token); @@ -114,7 +117,7 @@ export default async function Page(props: { } if (!idpInformation) { - return loginFailed(branding); + return loginFailed(branding, "IDP information missing"); } const idp = await getIDPByID(idpInformation.idpId); @@ -204,5 +207,5 @@ export default async function Page(props: { } // return login failed if no linking or creation is allowed and no user was found - return loginFailed(branding); + return loginFailed(branding, "No user found"); } From 6aa4137fe980b0d766c84e9fe579f5db6a10e4b4 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 10:41:37 +0100 Subject: [PATCH 14/18] register page cleanup --- .../src/app/(login)/idp/[provider]/success/page.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 9209f4a4..a2bebc3a 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -22,7 +22,7 @@ async function loginFailed(branding?: BrandingSettings, error: string = "") {

{t("loginError.title")}

-

{t("loginError.title")}

+

{t("loginError.description")}

{error && (
{{error}} @@ -199,7 +199,12 @@ export default async function Page(props: {

{t("registerSuccess.title")}

-
{t("registerSuccess.description")}
+

{t("registerSuccess.description")}

+
); From 53c08926144c76bb6e6a503abcbfba4c0fbead60 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 11:15:01 +0100 Subject: [PATCH 15/18] org discovery on idp callback --- .../(login)/idp/[provider]/success/page.tsx | 49 ++++++++++++++++++- apps/login/src/app/(login)/loginname/page.tsx | 2 +- apps/login/src/lib/zitadel.ts | 16 ++---- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index a2bebc3a..c81d43de 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -3,17 +3,27 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { IdpSignin } from "@/components/idp-signin"; import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp"; import { + addHuman, addIDPLink, - createUser, getBrandingSettings, getIDPByID, + getLoginSettings, + getOrgsByDomain, listUsers, retrieveIDPIntent, } from "@/lib/zitadel"; +import { create } from "@zitadel/client"; import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; +import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { + AddHumanUserRequest, + AddHumanUserRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { getLocale, getTranslations } from "next-intl/server"; +const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; + async function loginFailed(branding?: BrandingSettings, error: string = "") { const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); @@ -192,7 +202,42 @@ export default async function Page(props: { } if (options?.isCreationAllowed && options.isAutoCreation) { - const newUser = await createUser(providerType, idpInformation); + let orgToRegisterOn: string | undefined = organization; + + let userData: AddHumanUserRequest = + PROVIDER_MAPPING[providerType](idpInformation); + + if ( + !orgToRegisterOn && + userData.username && // username or email? + ORG_SUFFIX_REGEX.test(userData.username) + ) { + const matched = ORG_SUFFIX_REGEX.exec(userData.username); + 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; + } + } + + if (orgToRegisterOn) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: orgToRegisterOn }, + }); + + userData = create(AddHumanUserRequestSchema, { + ...userData, + organization: organizationSchema, + }); + } + + const newUser = await addHuman(userData); if (newUser) { return ( diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index e886de7f..44601e18 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -61,7 +61,7 @@ export default async function Page(props: { )} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 306d168b..aaf0a7c0 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -10,8 +10,8 @@ import { import { createServerTransport } from "@zitadel/node"; import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { + AddHumanUserRequest, RetrieveIdentityProviderIntentRequest, SetPasswordRequest, SetPasswordRequestSchema, @@ -23,7 +23,6 @@ import { create, Duration } 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 { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; -import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { NotificationType, @@ -39,7 +38,6 @@ import { UserState, } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { unstable_cacheLife as cacheLife } from "next/cache"; -import { PROVIDER_MAPPING } from "./idp"; const transport = createServerTransport( process.env.ZITADEL_SERVICE_USER_TOKEN!, @@ -249,6 +247,10 @@ export async function addHumanUser({ }); } +export async function addHuman(request: AddHumanUserRequest) { + return userService.addHumanUser(request); +} + export async function verifyTOTPRegistration(code: string, userId: string) { return userService.verifyTOTPRegistration({ code, userId }, {}); } @@ -487,14 +489,6 @@ export function addIDPLink( ); } -export function createUser( - provider: IdentityProviderType, - info: IDPInformation, -) { - const userData = PROVIDER_MAPPING[provider](info); - return userService.addHumanUser(userData, {}); -} - /** * * @param userId the id of the user where the email should be set From 01b7d475514351e8ef55c47c9af7d546b9fca1c3 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 11:42:57 +0100 Subject: [PATCH 16/18] consider idp to be a valid mfa --- apps/login/src/app/login/route.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index 61cc6e5c..f20009be 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -109,9 +109,10 @@ async function isSessionValid(session: Session): Promise { const otpSms = session.factors.otpSms?.verifiedAt; const totp = session.factors.totp?.verifiedAt; const webAuthN = session.factors.webAuthN?.verifiedAt; + const idp = session.factors.intent?.verifiedAt; // TODO: forceMFA should not consider this as valid factor // must have one single check - mfaValid = !!(otpEmail || otpSms || totp || webAuthN); + mfaValid = !!(otpEmail || otpSms || totp || webAuthN || idp); if (!mfaValid) { console.warn("Session has no valid multifactor", session.factors); } @@ -207,6 +208,8 @@ export async function GET(request: NextRequest) { const isValid = await isSessionValid(selectedSession); + console.log("Session is valid:", isValid); + if (!isValid && selectedSession.factors?.user) { // if the session is not valid anymore, we need to redirect the user to re-authenticate const command: SendLoginnameCommand = { From 5fab1ba08acb4b1d4c91d6f71a4c4889424162c0 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 16 Dec 2024 16:57:05 +0100 Subject: [PATCH 17/18] doc --- apps/login/readme.md | 2 ++ apps/login/src/app/login/route.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/login/readme.md b/apps/login/readme.md index f206fbf4..a411b996 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -396,3 +396,5 @@ Timebased features like the multifactor init prompt or password expiry, are not - Login Settings: multifactor init prompt - forceMFA on login settings is not checked for IDPs - disablePhone / disableEmail from loginSettings will be implemented right after https://github.com/zitadel/zitadel/issues/9016 is merged + +Also note that IDP logins are considered as valid MFA. An additional MFA check will be implemented in future if enforced. diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index f20009be..bbeda736 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -211,7 +211,8 @@ export async function GET(request: NextRequest) { console.log("Session is valid:", isValid); if (!isValid && selectedSession.factors?.user) { - // if the session is not valid anymore, we need to redirect the user to re-authenticate + // if the session is not valid anymore, we need to redirect the user to re-authenticate / + // TODO: handle IDP intent direcly if available const command: SendLoginnameCommand = { loginName: selectedSession.factors.user?.loginName, organization: selectedSession.factors?.user?.organizationId, From 0fa5752f4c1c2fd78df4f0dbe0d2c41f3c9f0358 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 17 Dec 2024 08:44:44 +0100 Subject: [PATCH 18/18] update idp list --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 429ce84c..3c47b35f 100644 --- a/README.md +++ b/README.md @@ -60,9 +60,9 @@ You can already use the current state, and extend it with your needs. - [x] GitLab - [x] GitLab Enterprise - [x] Azure - - [ ] Apple + - [x] Apple - [x] Generic OIDC - - [ ] Generic OAuth + - [x] Generic OAuth - [ ] Generic JWT - [ ] LDAP - [ ] SAML SP