Merge pull request #357 from zitadel/saml-2

feat: SAML
This commit is contained in:
Max Peintner
2025-02-20 10:11:13 +01:00
committed by GitHub
62 changed files with 971 additions and 744 deletions

View File

@@ -36,7 +36,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "accounts" });
const authRequestId = searchParams?.authRequestId;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const _headers = await headers();
@@ -62,8 +62,8 @@ export default async function Page(props: {
const params = new URLSearchParams();
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
@@ -77,7 +77,7 @@ export default async function Page(props: {
<p className="ztdl-p mb-6 block">{t("description")}</p>
<div className="flex flex-col w-full space-y-2">
<SessionsList sessions={sessions} authRequestId={authRequestId} />
<SessionsList sessions={sessions} requestId={requestId} />
<Link href={`/loginname?` + params}>
<div className="flex flex-row items-center py-3 px-4 hover:bg-black/10 dark:hover:bg-white/10 rounded-md transition-all">
<div className="w-8 h-8 mr-4 flex flex-row justify-center items-center rounded-full bg-black/5 dark:bg-white/5">

View File

@@ -27,7 +27,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "authenticator" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, organization, sessionId } = searchParams;
const { loginName, requestId, organization, sessionId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -141,8 +141,8 @@ export default async function Page(props: {
params.set("organization", sessionWithData.factors?.user?.organizationId);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
return (
@@ -174,7 +174,7 @@ export default async function Page(props: {
{loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
authRequestId={authRequestId}
requestId={requestId}
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>

View File

@@ -36,7 +36,7 @@ export default async function Page(props: {
const searchParams = await props.searchParams;
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const { id, token, authRequestId, organization, link } = searchParams;
const { id, token, requestId, organization, link } = searchParams;
const { provider } = params;
const _headers = await headers();
@@ -68,7 +68,7 @@ export default async function Page(props: {
return loginSuccess(
userId,
{ idpIntentId: id, idpIntentToken: token },
authRequestId,
requestId,
branding,
);
}
@@ -119,7 +119,7 @@ export default async function Page(props: {
return linkingSuccess(
userId,
{ idpIntentId: id, idpIntentToken: token },
authRequestId,
requestId,
branding,
);
}
@@ -177,7 +177,7 @@ export default async function Page(props: {
return linkingSuccess(
foundUser.userId,
{ idpIntentId: id, idpIntentToken: token },
authRequestId,
requestId,
branding,
);
}
@@ -243,7 +243,7 @@ export default async function Page(props: {
<IdpSignin
userId={newUser.userId}
idpIntent={{ idpIntentId: id, idpIntentToken: token }}
authRequestId={authRequestId}
requestId={requestId}
/>
</div>
</DynamicTheme>

View File

@@ -12,7 +12,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "idp" });
const authRequestId = searchParams?.authRequestId;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const _headers = await headers();
@@ -41,7 +41,7 @@ export default async function Page(props: {
{identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
></SignInWithIdp>
)}

View File

@@ -20,7 +20,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "loginname" });
const loginName = searchParams?.loginName;
const authRequestId = searchParams?.authRequestId;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const suffix = searchParams?.suffix;
const submit: boolean = searchParams?.submit === "true";
@@ -72,7 +72,7 @@ export default async function Page(props: {
<UsernameForm
loginName={loginName}
authRequestId={authRequestId}
requestId={requestId}
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
loginSettings={contextLoginSettings}
suffix={suffix}
@@ -82,7 +82,7 @@ export default async function Page(props: {
{identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
></SignInWithIdp>
)}

View File

@@ -22,7 +22,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, organization, sessionId } = searchParams;
const { loginName, requestId, organization, sessionId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -114,7 +114,7 @@ export default async function Page(props: {
<ChooseSecondFactor
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
userMethods={sessionFactors.authMethods ?? []}
></ChooseSecondFactor>

View File

@@ -42,14 +42,8 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "mfa" });
const tError = await getTranslations({ locale, namespace: "error" });
const {
loginName,
checkAfter,
force,
authRequestId,
organization,
sessionId,
} = searchParams;
const { loginName, checkAfter, force, requestId, organization, sessionId } =
searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -157,7 +151,7 @@ export default async function Page(props: {
<ChooseSecondFactorToSetup
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
loginSettings={loginSettings}
userMethods={sessionWithData.authMethods ?? []}

View File

@@ -34,7 +34,7 @@ export default async function Page(props: {
const {
loginName, // send from password page
userId, // send from email link
authRequestId,
requestId,
sessionId,
organization,
code,
@@ -115,7 +115,7 @@ export default async function Page(props: {
<LoginOTP
loginName={loginName ?? session.factors?.user?.loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={
organization ?? session?.factors?.user?.organizationId
}

View File

@@ -29,7 +29,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "otp" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, sessionId, authRequestId, checkAfter } =
const { loginName, organization, sessionId, requestId, checkAfter } =
searchParams;
const { method } = params;
@@ -111,22 +111,22 @@ export default async function Page(props: {
}
if (checkAfter) {
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
if (requestId) {
paramsToContinue.append("requestId", requestId);
}
urlToContinue = `/otp/${method}?` + paramsToContinue;
// immediately check the OTP on the next page if sms or email was set up
if (["email", "sms"].includes(method)) {
return redirect(urlToContinue);
}
} else if (authRequestId && sessionId) {
if (authRequestId) {
paramsToContinue.append("authRequest", authRequestId);
} else if (requestId && sessionId) {
if (requestId) {
paramsToContinue.append("authRequest", requestId);
}
urlToContinue = `/login?` + paramsToContinue;
} else if (loginName) {
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
if (requestId) {
paramsToContinue.append("requestId", requestId);
}
urlToContinue = `/signedin?` + paramsToContinue;
}
@@ -165,7 +165,7 @@ export default async function Page(props: {
secret={totpResponse.secret as string}
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
checkAfter={checkAfter === "true"}
loginSettings={loginSettings}

View File

@@ -17,7 +17,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, altPassword, authRequestId, organization, sessionId } =
const { loginName, altPassword, requestId, organization, sessionId } =
searchParams;
const _headers = await headers();
@@ -76,7 +76,7 @@ export default async function Page(props: {
<LoginPasskey
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
altPassword={altPassword === "true"}
organization={organization}
/>

View File

@@ -16,8 +16,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "passkey" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, prompt, organization, authRequestId, userId } =
searchParams;
const { loginName, prompt, organization, requestId, userId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -76,7 +75,7 @@ export default async function Page(props: {
sessionId={session.id}
isPrompt={!!prompt}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
/>
)}
</div>

View File

@@ -23,7 +23,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, authRequestId } = searchParams;
const { loginName, organization, requestId } = searchParams;
// also allow no session to be found (ignoreUnkownUsername)
const sessionFactors = await loadMostRecentSession({
@@ -84,7 +84,7 @@ export default async function Page(props: {
<ChangePasswordForm
sessionId={sessionFactors.id}
loginName={loginName}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
passwordComplexitySettings={passwordComplexity}
/>

View File

@@ -22,7 +22,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
let { loginName, organization, authRequestId, alt } = searchParams;
let { loginName, organization, requestId, alt } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -93,7 +93,7 @@ export default async function Page(props: {
{loginName && (
<PasswordForm
loginName={loginName}
authRequestId={authRequestId}
requestId={requestId}
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
loginSettings={loginSettings}
promptPasswordless={

View File

@@ -23,7 +23,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "password" });
const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, organization, authRequestId, code, initial } =
const { userId, loginName, organization, requestId, code, initial } =
searchParams;
const _headers = await headers();
@@ -113,7 +113,7 @@ export default async function Page(props: {
code={code}
userId={userId ?? (session?.factors?.user?.id as string)}
loginName={loginName ?? (user?.preferredLoginName as string)}
authRequestId={authRequestId}
requestId={requestId}
organization={organization}
passwordComplexitySettings={passwordComplexity}
codeRequired={!(initial === "true")}

View File

@@ -19,8 +19,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
let { firstname, lastname, email, organization, authRequestId } =
searchParams;
let { firstname, lastname, email, organization, requestId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -81,7 +80,7 @@ export default async function Page(props: {
firstname={firstname}
lastname={lastname}
email={email}
authRequestId={authRequestId}
requestId={requestId}
loginSettings={loginSettings}
></RegisterForm>
)}

View File

@@ -19,8 +19,7 @@ export default async function Page(props: {
const locale = getLocale();
const t = await getTranslations({ locale, namespace: "register" });
let { firstname, lastname, email, organization, authRequestId } =
searchParams;
let { firstname, lastname, email, organization, requestId } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -79,7 +78,7 @@ export default async function Page(props: {
firstname={firstname}
lastname={lastname}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
></SetRegisterPasswordForm>
)}
</div>

View File

@@ -6,6 +6,7 @@ import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service";
import {
createCallback,
createResponse,
getBrandingSettings,
getLoginSettings,
getSession,
@@ -15,6 +16,7 @@ import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import Link from "next/link";
@@ -24,16 +26,16 @@ async function loadSession(
serviceUrl: string,
loginName: string,
authRequestId?: string,
requestId?: string,
) {
const recent = await getMostRecentCookieWithLoginname({ loginName });
if (authRequestId) {
if (requestId && requestId.startsWith("oidc_")) {
return createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
authRequestId: requestId,
callbackKind: {
case: "session",
value: create(SessionSchema, {
@@ -45,7 +47,24 @@ async function loadSession(
}).then(({ callbackUrl }) => {
return redirect(callbackUrl);
});
} else if (requestId && requestId.startsWith("saml_")) {
return createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
value: {
sessionId: recent.id,
sessionToken: recent.token,
},
},
}),
}).then(({ url }) => {
return redirect(url);
});
}
return getSession({
serviceUrl,
@@ -66,12 +85,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { loginName, authRequestId, organization } = searchParams;
const { loginName, requestId, organization } = searchParams;
const sessionFactors = await loadSession(
serviceUrl,
loginName,
authRequestId,
requestId,
);
const branding = await getBrandingSettings({
@@ -81,7 +100,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
});
let loginSettings;
if (!authRequestId) {
if (!requestId) {
loginSettings = await getLoginSettings({
serviceUrl,

View File

@@ -17,7 +17,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, authRequestId, sessionId, organization } = searchParams;
const { loginName, requestId, sessionId, organization } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -80,7 +80,7 @@ export default async function Page(props: {
<LoginPasskey
loginName={loginName}
sessionId={sessionId}
authRequestId={authRequestId}
requestId={requestId}
altPassword={false}
organization={organization}
login={false} // this sets the userVerificationRequirement to discouraged as its used as second factor

View File

@@ -16,7 +16,7 @@ export default async function Page(props: {
const t = await getTranslations({ locale, namespace: "u2f" });
const tError = await getTranslations({ locale, namespace: "error" });
const { loginName, organization, authRequestId, checkAfter } = searchParams;
const { loginName, organization, requestId, checkAfter } = searchParams;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -62,7 +62,7 @@ export default async function Page(props: {
loginName={loginName}
sessionId={sessionFactors.id}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
checkAfter={checkAfter === "true"}
/>
)}

View File

@@ -22,7 +22,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const t = await getTranslations({ locale, namespace: "verify" });
const tError = await getTranslations({ locale, namespace: "error" });
const { userId, loginName, code, organization, authRequestId, invite } =
const { userId, loginName, code, organization, requestId, invite } =
searchParams;
const _headers = await headers();
@@ -63,7 +63,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId: sessionFactors?.factors?.user?.id,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not resend verification email", error);
throw Error("Failed to send verification email");
@@ -77,7 +77,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
(requestId ? `&requestId=${requestId}` : ""),
}).catch((error) => {
console.error("Could not resend verification email", error);
throw Error("Failed to send verification email");
@@ -120,8 +120,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
return (
@@ -165,7 +165,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId={id}
loginName={loginName}
organization={organization}
authRequestId={authRequestId}
requestId={requestId}
authMethods={authMethods}
/>
) : (
@@ -176,7 +176,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
userId={id}
code={code}
isInvite={invite === "true"}
authRequestId={authRequestId}
requestId={requestId}
/>
))}
</div>

View File

@@ -1,28 +1,28 @@
import { getAllSessions } from "@/lib/cookies";
import { idpTypeToSlug } from "@/lib/idp";
import { loginWithOIDCandSession } from "@/lib/oidc";
import { loginWithSAMLandSession } from "@/lib/saml";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { getServiceUrlFromHeaders } from "@/lib/service";
import { findValidSession } from "@/lib/session";
import {
createCallback,
createResponse,
getActiveIdentityProviders,
getAuthRequest,
getLoginSettings,
getOrgsByDomain,
listAuthenticationMethodTypes,
getSAMLRequest,
listSessions,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { create, timestampDate } from "@zitadel/client";
import {
AuthRequest,
Prompt,
} from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { create } from "@zitadel/client";
import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
@@ -30,12 +30,32 @@ export const dynamic = "force-dynamic";
export const revalidate = false;
export const fetchCache = "default-no-store";
const gotoAccounts = ({
request,
requestId,
organization,
}: {
request: NextRequest;
requestId: string;
organization?: string;
}): NextResponse<unknown> => {
const accountsUrl = new URL("/accounts", request.url);
if (requestId) {
accountsUrl.searchParams.set("requestId", requestId);
}
if (organization) {
accountsUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(accountsUrl);
};
async function loadSessions({
serviceUrl,
ids,
}: {
serviceUrl: string;
ids: string[];
}): Promise<Session[]> {
const response = await listSessions({
@@ -50,175 +70,23 @@ const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/;
const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options
const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/;
/**
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
**/
async function isSessionValid(
serviceUrl: string,
session: Session,
): Promise<boolean> {
// session can't be checked without user
if (!session.factors?.user) {
console.warn("Session has no user");
return false;
}
let mfaValid = true;
const authMethodTypes = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const authMethods = authMethodTypes.authMethodTypes;
if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) {
mfaValid = !!session.factors.totp?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid totpEmail factor",
session.factors.totp?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_EMAIL)
) {
mfaValid = !!session.factors.otpEmail?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpEmail factor",
session.factors.otpEmail?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_SMS)
) {
mfaValid = !!session.factors.otpSms?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpSms factor",
session.factors.otpSms?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.U2F)
) {
mfaValid = !!session.factors.webAuthN?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid u2f factor",
session.factors.webAuthN?.verifiedAt,
);
}
} else {
// only check settings if no auth methods are available, as this would require a setup
const loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors?.user?.organizationId,
});
if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) {
const otpEmail = session.factors.otpEmail?.verifiedAt;
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 || idp);
if (!mfaValid) {
console.warn("Session has no valid multifactor", session.factors);
}
} else {
mfaValid = true;
}
}
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validIDP = session?.factors?.intent?.verifiedAt;
const stillValid = session.expirationDate
? timestampDate(session.expirationDate).getTime() > new Date().getTime()
: true;
if (!stillValid) {
console.warn(
"Session is expired",
session.expirationDate
? timestampDate(session.expirationDate).toDateString()
: "no expiration date",
);
}
const validChecks = !!(validPassword || validPasskey || validIDP);
return stillValid && validChecks && mfaValid;
}
async function findValidSession(
serviceUrl: string,
sessions: Session[],
authRequest: AuthRequest,
): Promise<Session | undefined> {
const sessionsWithHint = sessions.filter((s) => {
if (authRequest.hintUserId) {
return s.factors?.user?.id === authRequest.hintUserId;
}
if (authRequest.loginHint) {
return s.factors?.user?.loginName === authRequest.loginHint;
}
return true;
});
if (sessionsWithHint.length === 0) {
return undefined;
}
// sort by change date descending
sessionsWithHint.sort((a, b) => {
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
return dateB - dateA;
});
// return the first valid session according to settings
for (const session of sessionsWithHint) {
if (await isSessionValid(serviceUrl, session)) {
return session;
}
}
return undefined;
}
function constructUrl(request: NextRequest, path: string) {
const forwardedHost =
request.headers.get("x-zitadel-forward-host") ??
request.headers.get("host");
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
return new URL(
`${basePath}${path}`,
forwardedHost?.startsWith("http")
? forwardedHost
: `https://${forwardedHost}`,
);
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const authRequestId = searchParams.get("authRequest");
const sessionId = searchParams.get("sessionId");
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const searchParams = request.nextUrl.searchParams;
const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request
const samlRequestId = searchParams.get("samlRequest"); // saml initiated request
// internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_
let requestId =
searchParams.get("requestId") ||
`oidc_${oidcRequestId}` ||
`saml_${samlRequestId}`;
const sessionId = searchParams.get("sessionId");
// TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback
const _rsc = searchParams.get("_rsc");
if (_rsc) {
@@ -232,128 +100,36 @@ export async function GET(request: NextRequest) {
sessions = await loadSessions({ serviceUrl, ids });
}
if (authRequestId && sessionId) {
console.log(
`Login with session: ${sessionId} and authRequest: ${authRequestId}`,
);
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid(
// complete flow if session and request id are provided
if (requestId && sessionId) {
if (requestId.startsWith("oidc_")) {
// this finishes the login process for OIDC
await loginWithOIDCandSession({
serviceUrl,
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 /
// TODO: handle IDP intent direcly if available
const command: SendLoginnameCommand = {
loginName: selectedSession.factors.user?.loginName,
organization: selectedSession.factors?.user?.organizationId,
authRequestId: authRequestId,
};
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.nextUrl);
return NextResponse.redirect(absoluteUrl.toString());
}
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
// works not with _rsc request
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error: unknown) {
// handle already handled gracefully as these could come up if old emails with authRequestId are used (reset password, register emails etc.)
console.error(error);
if (
error &&
typeof error === "object" &&
"code" in error &&
error?.code === 9
) {
const loginSettings = await getLoginSettings({
serviceUrl,
organization: selectedSession.factors?.user?.organizationId,
});
if (loginSettings?.defaultRedirectUri) {
return NextResponse.redirect(loginSettings.defaultRedirectUri);
}
const signedinUrl = constructUrl(request, "/signedin");
const params = new URLSearchParams();
if (selectedSession.factors?.user?.loginName) {
params.append(
"loginName",
selectedSession.factors?.user?.loginName,
);
// signedinUrl.searchParams.set(
// "loginName",
// selectedSession.factors?.user?.loginName,
// );
}
if (selectedSession.factors?.user?.organizationId) {
params.append(
"organization",
selectedSession.factors?.user?.organizationId,
);
// signedinUrl.searchParams.set(
// "organization",
// selectedSession.factors?.user?.organizationId,
// );
}
return NextResponse.redirect(signedinUrl + "?" + params);
} else {
return NextResponse.json({ error }, { status: 500 });
}
}
}
authRequest: requestId.replace("oidc_", ""),
sessionId,
sessions,
sessionCookies,
request,
});
} else if (requestId.startsWith("saml_")) {
// this finishes the login process for SAML
await loginWithSAMLandSession({
serviceUrl,
samlRequest: requestId.replace("saml_", ""),
sessionId,
sessions,
sessionCookies,
request,
});
}
}
if (authRequestId) {
// continue with OIDC
if (requestId && requestId.startsWith("oidc_")) {
const { authRequest } = await getAuthRequest({
serviceUrl,
authRequestId,
authRequestId: requestId.replace("oidc_", ""),
});
let organization = "";
@@ -400,7 +176,6 @@ export async function GET(request: NextRequest) {
const identityProviders = await getActiveIdentityProviders({
serviceUrl,
orgId: organization ? organization : undefined,
}).then((resp) => {
return resp.identityProviders;
@@ -416,8 +191,8 @@ export async function GET(request: NextRequest) {
const params = new URLSearchParams();
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
if (organization) {
@@ -426,7 +201,6 @@ export async function GET(request: NextRequest) {
return startIdentityProviderFlow({
serviceUrl,
idpId,
urls: {
successUrl:
@@ -448,41 +222,27 @@ export async function GET(request: NextRequest) {
}
}
const gotoAccounts = (): NextResponse<unknown> => {
const accountsUrl = constructUrl(request, "/accounts");
const params = new URLSearchParams();
if (authRequest?.id) {
params.append("authRequestId", authRequest.id);
// accountsUrl.searchParams.set("authRequestId", authRequest?.id);
}
if (organization) {
params.append("organization", organization);
// accountsUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(accountsUrl + "?" + params);
};
if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
const registerUrl = constructUrl(request, "/register");
const params = new URLSearchParams();
const registerUrl = new URL("/register", request.url);
if (authRequest.id) {
params.append("authRequestId", authRequest.id);
// registerUrl.searchParams.set("authRequestId", authRequest.id);
registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
}
if (organization) {
params.append("organization", organization);
// registerUrl.searchParams.set("organization", organization);
registerUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(registerUrl + "?" + params);
return NextResponse.redirect(registerUrl);
}
// use existing session and hydrate it for oidc
if (authRequest && sessions.length) {
// if some accounts are available for selection and select_account is set
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
/**
* The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated
@@ -493,7 +253,7 @@ export async function GET(request: NextRequest) {
try {
let command: SendLoginnameCommand = {
loginName: authRequest.loginHint,
authRequestId: authRequest.id,
requestId: authRequest.id,
};
if (organization) {
@@ -503,7 +263,7 @@ export async function GET(request: NextRequest) {
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.nextUrl);
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
} catch (error) {
@@ -511,39 +271,31 @@ export async function GET(request: NextRequest) {
}
}
const loginNameUrl = constructUrl(request, "/loginname");
const params = new URLSearchParams();
const loginNameUrl = new URL("/loginname", request.url);
if (authRequest.id) {
params.append("authRequestId", authRequest.id);
// loginNameUrl.searchParams.set("authRequestId", authRequest.id);
loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
}
if (authRequest.loginHint) {
params.append("loginName", authRequest.loginHint);
// loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
}
if (organization) {
params.append("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
params.append("suffix", suffix);
// loginNameUrl.searchParams.set("suffix", suffix);
loginNameUrl.searchParams.set("suffix", suffix);
}
return NextResponse.redirect(loginNameUrl + "?" + params);
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
/**
* With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages.
* This means that the user should not be prompted to enter their password again.
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
**/
const selectedSession = await findValidSession(
const selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
);
});
if (!selectedSession || !selectedSession.id) {
return NextResponse.json(
@@ -570,9 +322,8 @@ export async function GET(request: NextRequest) {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
value: create(SessionSchema, session),
@@ -582,15 +333,18 @@ export async function GET(request: NextRequest) {
return NextResponse.redirect(callbackUrl);
} else {
// check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession(
let selectedSession = await findValidSession({
serviceUrl,
sessions,
authRequest,
);
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
const cookie = sessionCookies.find(
@@ -598,7 +352,11 @@ export async function GET(request: NextRequest) {
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
const session = {
@@ -611,7 +369,7 @@ export async function GET(request: NextRequest) {
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId,
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
value: create(SessionSchema, session),
@@ -624,36 +382,148 @@ export async function GET(request: NextRequest) {
console.log(
"could not create callback, redirect user to choose other account",
);
return gotoAccounts();
return gotoAccounts({
request,
organization,
requestId: `oidc_${authRequest.id}`,
});
}
} catch (error) {
console.error(error);
return gotoAccounts();
return gotoAccounts({
request,
requestId: `oidc_${authRequest.id}`,
organization,
});
}
}
} else {
const loginNameUrl = constructUrl(request, "/loginname");
const loginNameUrl = new URL("/loginname", request.url);
const params = new URLSearchParams();
params.set("authRequestId", authRequestId);
// loginNameUrl.searchParams.set("authRequestId", authRequestId);
loginNameUrl.searchParams.set("requestId", requestId);
if (authRequest?.loginHint) {
params.set("loginName", authRequest.loginHint);
params.set("submit", "true"); // autosubmit
// loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
// loginNameUrl.searchParams.set("submit", "true"); // autosubmit
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
}
if (organization) {
params.set("organization", organization);
loginNameUrl.searchParams.append("organization", organization);
// loginNameUrl.searchParams.set("organization", organization);
}
return NextResponse.redirect(loginNameUrl + "?" + params);
return NextResponse.redirect(loginNameUrl);
}
}
// continue with SAML
else if (requestId && requestId.startsWith("saml_")) {
const { samlRequest } = await getSAMLRequest({
serviceUrl,
samlRequestId: requestId.replace("saml_", ""),
});
if (!samlRequest) {
return NextResponse.json(
{ error: "No samlRequest found" },
{ status: 400 },
);
}
let selectedSession = await findValidSession({
serviceUrl,
sessions,
samlRequest,
});
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession.id,
);
if (!cookie || !cookie.id || !cookie.token) {
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
// organization,
});
}
const session = {
sessionId: cookie.id,
sessionToken: cookie.token,
};
try {
const { url, binding } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
value: session,
},
}),
});
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
const formData = {
key1: "value1",
key2: "value2",
};
// Convert form data to URL-encoded string
const formBody = Object.entries(formData)
.map(
([key, value]) =>
encodeURIComponent(key) + "=" + encodeURIComponent(value),
)
.join("&");
// Make a POST request to the external URL with the form data
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody,
});
// Handle the response from the external URL
if (response.ok) {
return NextResponse.json({
message: "SAML request completed successfully",
});
} else {
return NextResponse.json(
{ error: "Failed to complete SAML request" },
{ status: response.status },
);
}
} else {
console.log(
"could not create response, redirect user to choose other account",
);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
} catch (error) {
console.error(error);
return gotoAccounts({
request,
requestId: `saml_${samlRequest.id}`,
});
}
} else {
return NextResponse.json(
{ error: "No authRequestId provided" },
{ error: "No authRequest nor samlRequest provided" },
{ status: 500 },
);
}

View File

@@ -35,7 +35,7 @@ type Props = {
passwordComplexitySettings: PasswordComplexitySettings;
sessionId: string;
loginName: string;
authRequestId?: string;
requestId?: string;
organization?: string;
};
@@ -43,7 +43,7 @@ export function ChangePasswordForm({
passwordComplexitySettings,
sessionId,
loginName,
authRequestId,
requestId,
organization,
}: Props) {
const t = useTranslations("password");
@@ -97,7 +97,7 @@ export function ChangePasswordForm({
checks: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify password");

View File

@@ -10,7 +10,7 @@ import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
type Props = {
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
loginSettings: LoginSettings;
userMethods: AuthenticationMethodType[];
@@ -22,7 +22,7 @@ type Props = {
export function ChooseSecondFactorToSetup({
loginName,
sessionId,
authRequestId,
requestId,
organization,
loginSettings,
userMethods,
@@ -38,8 +38,8 @@ export function ChooseSecondFactorToSetup({
if (sessionId) {
params.append("sessionId", sessionId);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);

View File

@@ -6,7 +6,7 @@ import { EMAIL, SMS, TOTP, U2F } from "./auth-methods";
type Props = {
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
userMethods: AuthenticationMethodType[];
};
@@ -14,7 +14,7 @@ type Props = {
export function ChooseSecondFactor({
loginName,
sessionId,
authRequestId,
requestId,
organization,
userMethods,
}: Props) {
@@ -26,8 +26,8 @@ export function ChooseSecondFactor({
if (sessionId) {
params.append("sessionId", sessionId);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);

View File

@@ -13,13 +13,13 @@ type Props = {
idpIntentId: string;
idpIntentToken: string;
};
authRequestId?: string;
requestId?: string;
};
export function IdpSignin({
userId,
idpIntent: { idpIntentId, idpIntentToken },
authRequestId,
requestId,
}: Props) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -33,7 +33,7 @@ export function IdpSignin({
idpIntentId,
idpIntentToken,
},
authRequestId,
requestId,
})
.then((response) => {
if (response && "error" in response && response?.error) {

View File

@@ -6,7 +6,7 @@ import { IdpSignin } from "../../idp-signin";
export async function linkingSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
requestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
@@ -21,7 +21,7 @@ export async function linkingSuccess(
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
requestId={requestId}
/>
</div>
</DynamicTheme>

View File

@@ -6,7 +6,7 @@ import { IdpSignin } from "../../idp-signin";
export async function loginSuccess(
userId: string,
idpIntent: { idpIntentId: string; idpIntentToken: string },
authRequestId?: string,
requestId?: string,
branding?: BrandingSettings,
) {
const locale = getLocale();
@@ -21,7 +21,7 @@ export async function loginSuccess(
<IdpSignin
userId={userId}
idpIntent={idpIntent}
authRequestId={authRequestId}
requestId={requestId}
/>
</div>
</DynamicTheme>

View File

@@ -21,7 +21,7 @@ type Props = {
host: string | null;
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
method: string;
code?: string;
@@ -36,7 +36,7 @@ export function LoginOTP({
host,
loginName,
sessionId,
authRequestId,
requestId,
organization,
method,
code,
@@ -85,7 +85,7 @@ export function LoginOTP({
? {
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/otp/${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` +
(authRequestId ? `&authRequestId=${authRequestId}` : ""),
(requestId ? `&requestId=${requestId}` : ""),
}
: {},
},
@@ -105,7 +105,7 @@ export function LoginOTP({
sessionId,
organization,
challenges,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not request OTP challenge");
@@ -135,8 +135,8 @@ export function LoginOTP({
body.organization = organization;
}
if (authRequestId) {
body.authRequestId = authRequestId;
if (requestId) {
body.requestId = requestId;
}
let checks;
@@ -162,7 +162,7 @@ export function LoginOTP({
sessionId,
organization,
checks,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify OTP code");
@@ -188,11 +188,11 @@ export function LoginOTP({
await new Promise((resolve) => setTimeout(resolve, 2000));
const url =
authRequestId && response.sessionId
requestId && response.sessionId
? await getNextUrl(
{
sessionId: response.sessionId,
authRequestId: authRequestId,
requestId: requestId,
organization: response.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,

View File

@@ -21,7 +21,7 @@ import { Spinner } from "./spinner";
type Props = {
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
altPassword: boolean;
login?: boolean;
organization?: string;
@@ -30,7 +30,7 @@ type Props = {
export function LoginPasskey({
loginName,
sessionId,
authRequestId,
requestId,
altPassword,
organization,
login = true,
@@ -96,7 +96,7 @@ export function LoginPasskey({
userVerificationRequirement,
},
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not request passkey challenge");
@@ -123,7 +123,7 @@ export function LoginPasskey({
checks: {
webAuthN: { credentialAssertionData: data },
} as Checks,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify passkey");
@@ -220,8 +220,8 @@ export function LoginPasskey({
params.sessionId = sessionId;
}
if (authRequestId) {
params.authRequestId = authRequestId;
if (requestId) {
params.requestId = requestId;
}
if (organization) {

View File

@@ -22,7 +22,7 @@ type Props = {
loginSettings: LoginSettings | undefined;
loginName: string;
organization?: string;
authRequestId?: string;
requestId?: string;
isAlternative?: boolean; // whether password was requested as alternative auth method
promptPasswordless?: boolean;
};
@@ -31,7 +31,7 @@ export function PasswordForm({
loginSettings,
loginName,
organization,
authRequestId,
requestId,
promptPasswordless,
isAlternative,
}: Props) {
@@ -58,7 +58,7 @@ export function PasswordForm({
checks: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify password");
@@ -86,7 +86,7 @@ export function PasswordForm({
const response = await resetPassword({
loginName,
organization,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not reset password");
@@ -111,8 +111,8 @@ export function PasswordForm({
params.append("organization", organization);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
return router.push("/password/set?" + params);

View File

@@ -36,7 +36,7 @@ type Props = {
lastname?: string;
email?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
loginSettings?: LoginSettings;
};
@@ -46,7 +46,7 @@ export function RegisterForm({
firstname,
lastname,
organization,
authRequestId,
requestId,
loginSettings,
}: Props) {
const t = useTranslations("register");
@@ -73,7 +73,7 @@ export function RegisterForm({
firstName: values.firstname,
lastName: values.lastname,
organization: organization,
authRequestId: authRequestId,
requestId: requestId,
})
.catch(() => {
setError("Could not register user");
@@ -105,8 +105,8 @@ export function RegisterForm({
registerParams.organization = organization;
}
if (authRequestId) {
registerParams.authRequestId = authRequestId;
if (requestId) {
registerParams.requestId = requestId;
}
// redirect user to /register/password if password is chosen

View File

@@ -19,7 +19,7 @@ type Inputs = {};
type Props = {
sessionId: string;
isPrompt: boolean;
authRequestId?: string;
requestId?: string;
organization?: string;
};
@@ -27,7 +27,7 @@ export function RegisterPasskey({
sessionId,
isPrompt,
organization,
authRequestId,
requestId,
}: Props) {
const t = useTranslations("passkey");
@@ -161,8 +161,8 @@ export function RegisterPasskey({
params.set("organization", organization);
}
if (authRequestId) {
params.set("authRequestId", authRequestId);
if (requestId) {
params.set("requestId", requestId);
}
params.set("sessionId", sessionId);

View File

@@ -16,7 +16,7 @@ import { Spinner } from "./spinner";
type Props = {
loginName?: string;
sessionId: string;
authRequestId?: string;
requestId?: string;
organization?: string;
checkAfter: boolean;
loginSettings?: LoginSettings;
@@ -26,7 +26,7 @@ export function RegisterU2f({
loginName,
sessionId,
organization,
authRequestId,
requestId,
checkAfter,
loginSettings,
}: Props) {
@@ -166,18 +166,18 @@ export function RegisterU2f({
if (organization) {
paramsToContinue.append("organization", organization);
}
if (authRequestId) {
paramsToContinue.append("authRequestId", authRequestId);
if (requestId) {
paramsToContinue.append("requestId", requestId);
}
return router.push(`/u2f?` + paramsToContinue);
} else {
const url =
authRequestId && sessionId
requestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
authRequestId: authRequestId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,

View File

@@ -31,11 +31,11 @@ export function isSessionValid(session: Partial<Session>): {
export function SessionItem({
session,
reload,
authRequestId,
requestId,
}: {
session: Session;
reload: () => void;
authRequestId?: string;
requestId?: string;
}) {
const [loading, setLoading] = useState<boolean>(false);
@@ -67,7 +67,7 @@ export function SessionItem({
if (valid && session?.factors?.user) {
const resp = await continueWithSession({
...session,
authRequestId: authRequestId,
requestId: requestId,
});
if (resp?.redirect) {
@@ -78,7 +78,7 @@ export function SessionItem({
const res = await sendLoginname({
loginName: session.factors?.user?.loginName,
organization: session.factors.user.organizationId,
authRequestId: authRequestId,
requestId: requestId,
})
.catch(() => {
setError("An internal error occurred");

View File

@@ -9,10 +9,10 @@ import { SessionItem } from "./session-item";
type Props = {
sessions: Session[];
authRequestId?: string;
requestId?: string;
};
export function SessionsList({ sessions, authRequestId }: Props) {
export function SessionsList({ sessions, requestId }: Props) {
const t = useTranslations("accounts");
const [list, setList] = useState<Session[]>(sessions);
return sessions ? (
@@ -34,7 +34,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
return (
<SessionItem
session={session}
authRequestId={authRequestId}
requestId={requestId}
reload={() => {
setList(list.filter((s) => s.id !== session.id));
}}

View File

@@ -39,14 +39,14 @@ type Props = {
loginName: string;
userId: string;
organization?: string;
authRequestId?: string;
requestId?: string;
codeRequired: boolean;
};
export function SetPasswordForm({
passwordComplexitySettings,
organization,
authRequestId,
requestId,
loginName,
userId,
code,
@@ -73,7 +73,7 @@ export function SetPasswordForm({
const response = await resetPassword({
loginName,
organization,
authRequestId,
requestId,
})
.catch(() => {
setError("Could not reset password");
@@ -137,7 +137,7 @@ export function SetPasswordForm({
checks: create(ChecksSchema, {
password: { password: values.password },
}),
authRequestId,
requestId,
})
.catch(() => {
setError("Could not verify password");

View File

@@ -32,7 +32,7 @@ type Props = {
firstname: string;
lastname: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export function SetRegisterPasswordForm({
@@ -41,7 +41,7 @@ export function SetRegisterPasswordForm({
firstname,
lastname,
organization,
authRequestId,
requestId,
}: Props) {
const t = useTranslations("register");
@@ -66,7 +66,7 @@ export function SetRegisterPasswordForm({
firstName: firstname,
lastName: lastname,
organization: organization,
authRequestId: authRequestId,
requestId: requestId,
password: values.password,
})
.catch(() => {

View File

@@ -20,14 +20,14 @@ import { SignInWithGoogle } from "./idps/sign-in-with-google";
export interface SignInWithIDPProps {
children?: ReactNode;
identityProviders: IdentityProvider[];
authRequestId?: string;
requestId?: string;
organization?: string;
linkOnly?: boolean;
}
export function SignInWithIdp({
identityProviders,
authRequestId,
requestId,
organization,
linkOnly,
}: Readonly<SignInWithIDPProps>) {
@@ -40,7 +40,7 @@ export function SignInWithIdp({
setLoading(true);
const params = new URLSearchParams();
if (linkOnly) params.set("link", "true");
if (authRequestId) params.set("authRequestId", authRequestId);
if (requestId) params.set("requestId", requestId);
if (organization) params.set("organization", organization);
try {
@@ -64,7 +64,7 @@ export function SignInWithIdp({
setLoading(false);
}
},
[authRequestId, organization, linkOnly, router],
[requestId, organization, linkOnly, router],
);
const renderIDPButton = (idp: IdentityProvider) => {

View File

@@ -24,7 +24,7 @@ type Props = {
secret: string;
loginName?: string;
sessionId?: string;
authRequestId?: string;
requestId?: string;
organization?: string;
checkAfter?: boolean;
loginSettings?: LoginSettings;
@@ -34,7 +34,7 @@ export function TotpRegister({
secret,
loginName,
sessionId,
authRequestId,
requestId,
organization,
checkAfter,
loginSettings,
@@ -63,8 +63,8 @@ export function TotpRegister({
if (loginName) {
params.append("loginName", loginName);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization) {
params.append("organization", organization);
@@ -73,11 +73,11 @@ export function TotpRegister({
return router.push(`/otp/time-based?` + params);
} else {
const url =
authRequestId && sessionId
requestId && sessionId
? await getNextUrl(
{
sessionId: sessionId,
authRequestId: authRequestId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,

View File

@@ -25,8 +25,8 @@ export function UserAvatar({
params.set("organization", searchParams.organization);
}
if (searchParams?.authRequestId) {
params.set("authRequestId", searchParams.authRequestId);
if (searchParams?.requestId) {
params.set("requestId", searchParams.requestId);
}
if (searchParams?.loginName) {

View File

@@ -18,7 +18,7 @@ type Inputs = {
type Props = {
loginName: string | undefined;
authRequestId: string | undefined;
requestId: string | undefined;
loginSettings: LoginSettings | undefined;
organization?: string;
suffix?: string;
@@ -29,7 +29,7 @@ type Props = {
export function UsernameForm({
loginName,
authRequestId,
requestId,
organization,
suffix,
loginSettings,
@@ -56,7 +56,7 @@ export function UsernameForm({
const res = await sendLoginname({
loginName: values.loginName,
organization,
authRequestId,
requestId,
suffix,
})
.catch(() => {
@@ -117,8 +117,8 @@ export function UsernameForm({
if (organization) {
registerParams.append("organization", organization);
}
if (authRequestId) {
registerParams.append("authRequestId", authRequestId);
if (requestId) {
registerParams.append("requestId", requestId);
}
router.push("/register?" + registerParams);

View File

@@ -21,14 +21,14 @@ type Props = {
organization?: string;
code?: string;
isInvite: boolean;
authRequestId?: string;
requestId?: string;
};
export function VerifyForm({
userId,
loginName,
organization,
authRequestId,
requestId,
code,
isInvite,
}: Props) {
@@ -78,7 +78,7 @@ export function VerifyForm({
isInvite: isInvite,
loginName: loginName,
organization: organization,
authRequestId: authRequestId,
requestId: requestId,
})
.catch(() => {
setError("Could not verify user");

View File

@@ -15,13 +15,13 @@ import { Spinner } from "./spinner";
export function VerifyRedirectButton({
userId,
loginName,
authRequestId,
requestId,
authMethods,
organization,
}: {
userId?: string;
loginName?: string;
authRequestId: string;
requestId: string;
authMethods: AuthenticationMethodType[] | null;
organization?: string;
}) {
@@ -35,7 +35,7 @@ export function VerifyRedirectButton({
let command = {
organization,
authRequestId,
requestId,
} as SendVerificationRedirectWithoutCheckCommand;
if (userId) {

View File

@@ -1,12 +1,12 @@
type FinishFlowCommand =
| {
sessionId: string;
authRequestId: string;
requestId: string;
}
| { loginName: string };
/**
* for client: redirects user back to OIDC application or to a success page when using authRequestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
* for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
* @param command
* @returns
*/
@@ -14,10 +14,10 @@ export async function getNextUrl(
command: FinishFlowCommand & { organization?: string },
defaultRedirectUri?: string,
): Promise<string> {
if ("sessionId" in command && "authRequestId" in command) {
if ("sessionId" in command && "requestId" in command) {
const params = new URLSearchParams({
sessionId: command.sessionId,
authRequest: command.authRequestId,
requestId: command.requestId,
});
if (command.organization) {

View File

@@ -15,7 +15,7 @@ export type Cookie = {
creationTs: string;
expirationTs: string;
changeTs: string;
authRequestId?: string; // if its linked to an OIDC flow
requestId?: string; // if its linked to an OIDC flow
};
type SessionCookie<T> = Cookie & T;

131
apps/login/src/lib/oidc.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Cookie } from "@/lib/cookies";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { createCallback, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import {
CreateCallbackRequestSchema,
SessionSchema,
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server";
import { isSessionValid } from "./session";
type LoginWithOIDCandSession = {
serviceUrl: string;
authRequest: string;
sessionId: string;
sessions: Session[];
sessionCookies: Cookie[];
request: NextRequest;
};
export async function loginWithOIDCandSession({
serviceUrl,
authRequest,
sessionId,
sessions,
sessionCookies,
request,
}: LoginWithOIDCandSession) {
console.log(
`Login with session: ${sessionId} and authRequest: ${authRequest}`,
);
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid({
serviceUrl,
session: 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 /
// TODO: handle IDP intent direcly if available
const command: SendLoginnameCommand = {
loginName: selectedSession.factors.user?.loginName,
organization: selectedSession.factors?.user?.organizationId,
requestId: `oidc_${authRequest}`,
};
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
// works not with _rsc request
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
authRequestId: authRequest,
callbackKind: {
case: "session",
value: create(SessionSchema, session),
},
}),
});
if (callbackUrl) {
return NextResponse.redirect(callbackUrl);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error: unknown) {
// handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
console.error(error);
if (
error &&
typeof error === "object" &&
"code" in error &&
error?.code === 9
) {
const loginSettings = await getLoginSettings({
serviceUrl,
organization: selectedSession.factors?.user?.organizationId,
});
if (loginSettings?.defaultRedirectUri) {
return NextResponse.redirect(loginSettings.defaultRedirectUri);
}
const signedinUrl = new URL("/signedin", request.url);
if (selectedSession.factors?.user?.loginName) {
signedinUrl.searchParams.set(
"loginName",
selectedSession.factors?.user?.loginName,
);
}
if (selectedSession.factors?.user?.organizationId) {
signedinUrl.searchParams.set(
"organization",
selectedSession.factors?.user?.organizationId,
);
}
return NextResponse.redirect(signedinUrl);
} else {
return NextResponse.json({ error }, { status: 500 });
}
}
}
}
}

129
apps/login/src/lib/saml.ts Normal file
View File

@@ -0,0 +1,129 @@
import { Cookie } from "@/lib/cookies";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { createResponse, getLoginSettings } from "@/lib/zitadel";
import { create } from "@zitadel/client";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server";
import { isSessionValid } from "./session";
type LoginWithSAMLandSession = {
serviceUrl: string;
samlRequest: string;
sessionId: string;
sessions: Session[];
sessionCookies: Cookie[];
request: NextRequest;
};
export async function loginWithSAMLandSession({
serviceUrl,
samlRequest,
sessionId,
sessions,
sessionCookies,
request,
}: LoginWithSAMLandSession) {
console.log(
`Login with session: ${sessionId} and samlRequest: ${samlRequest}`,
);
const selectedSession = sessions.find((s) => s.id === sessionId);
if (selectedSession && selectedSession.id) {
console.log(`Found session ${selectedSession.id}`);
const isValid = await isSessionValid({
serviceUrl,
session: 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 /
// TODO: handle IDP intent direcly if available
const command: SendLoginnameCommand = {
loginName: selectedSession.factors.user?.loginName,
organization: selectedSession.factors?.user?.organizationId,
requestId: `saml_${samlRequest}`,
};
const res = await sendLoginname(command);
if (res && "redirect" in res && res?.redirect) {
const absoluteUrl = new URL(res.redirect, request.url);
return NextResponse.redirect(absoluteUrl.toString());
}
}
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);
if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
// works not with _rsc request
try {
const { url } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
samlRequestId: samlRequest,
responseKind: {
case: "session",
value: session,
},
}),
});
if (url) {
return NextResponse.redirect(url);
} else {
return NextResponse.json(
{ error: "An error occurred!" },
{ status: 500 },
);
}
} catch (error: unknown) {
// handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
console.error(error);
if (
error &&
typeof error === "object" &&
"code" in error &&
error?.code === 9
) {
const loginSettings = await getLoginSettings({
serviceUrl,
organization: selectedSession.factors?.user?.organizationId,
});
if (loginSettings?.defaultRedirectUri) {
return NextResponse.redirect(loginSettings.defaultRedirectUri);
}
const signedinUrl = new URL("/signedin", request.url);
if (selectedSession.factors?.user?.loginName) {
signedinUrl.searchParams.set(
"loginName",
selectedSession.factors?.user?.loginName,
);
}
if (selectedSession.factors?.user?.organizationId) {
signedinUrl.searchParams.set(
"organization",
selectedSession.factors?.user?.organizationId,
);
}
return NextResponse.redirect(signedinUrl);
} else {
return NextResponse.json({ error }, { status: 500 });
}
}
}
}
}

View File

@@ -30,7 +30,7 @@ type CustomCookieData = {
creationTs: string;
expirationTs: string;
changeTs: string;
authRequestId?: string; // if its linked to an OIDC flow
requestId?: string; // if its linked to an OIDC flow
};
const passwordAttemptsHandler = (error: ConnectError) => {
@@ -48,8 +48,7 @@ const passwordAttemptsHandler = (error: ConnectError) => {
export async function createSessionAndUpdateCookie(
checks: Checks,
challenges: RequestChallenges | undefined,
authRequestId: string | undefined,
requestId: string | undefined,
lifetime?: Duration,
): Promise<Session> {
const _headers = await headers();
@@ -57,9 +56,7 @@ export async function createSessionAndUpdateCookie(
const createdSession = await createSessionFromChecks({
serviceUrl,
checks,
challenges,
lifetime,
});
@@ -86,8 +83,8 @@ export async function createSessionAndUpdateCookie(
loginName: response.session.factors.user.loginName ?? "",
};
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
if (requestId) {
sessionCookie.requestId = requestId;
}
if (response.session.factors.user.organizationId) {
@@ -113,7 +110,7 @@ export async function createSessionForIdpAndUpdateCookie(
idpIntentId?: string | undefined;
idpIntentToken?: string | undefined;
},
authRequestId: string | undefined,
requestId: string | undefined,
lifetime?: Duration,
): Promise<Session> {
const _headers = await headers();
@@ -165,8 +162,8 @@ export async function createSessionForIdpAndUpdateCookie(
organization: session.factors.user.organizationId ?? "",
};
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
if (requestId) {
sessionCookie.requestId = requestId;
}
if (session.factors.user.organizationId) {
@@ -186,7 +183,7 @@ export async function setSessionAndUpdateCookie(
recentCookie: CustomCookieData,
checks?: Checks,
challenges?: RequestChallenges,
authRequestId?: string,
requestId?: string,
lifetime?: Duration,
) {
const _headers = await headers();
@@ -216,8 +213,8 @@ export async function setSessionAndUpdateCookie(
organization: recentCookie.organization,
};
if (authRequestId) {
sessionCookie.authRequestId = authRequestId;
if (requestId) {
sessionCookie.requestId = requestId;
}
return getSession({
@@ -241,8 +238,8 @@ export async function setSessionAndUpdateCookie(
organization: session.factors?.user?.organizationId ?? "",
};
if (sessionCookie.authRequestId) {
newCookie.authRequestId = sessionCookie.authRequestId;
if (sessionCookie.requestId) {
newCookie.requestId = sessionCookie.requestId;
}
return updateSessionCookie(sessionCookie.id, newCookie).then(() => {

View File

@@ -54,7 +54,7 @@ type CreateNewSessionCommand = {
loginName?: string;
password?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export async function createNewSessionFromIdpIntent(
@@ -91,7 +91,7 @@ export async function createNewSessionFromIdpIntent(
const session = await createSessionForIdpAndUpdateCookie(
command.userId,
command.idpIntent,
command.authRequestId,
command.requestId,
loginSettings?.externalLoginCheckLifetime,
);
@@ -109,7 +109,7 @@ export async function createNewSessionFromIdpIntent(
session,
humanUser,
command.organization,
command.authRequestId,
command.requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -117,16 +117,16 @@ export async function createNewSessionFromIdpIntent(
}
// TODO: check if user has MFA methods
// const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId);
// const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, requestId);
// if (mfaFactorCheck?.redirect) {
// return mfaFactorCheck;
// }
const url = await getNextUrl(
command.authRequestId && session.id
command.requestId && session.id
? {
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization: session.factors.user.organizationId,
}
: {

View File

@@ -11,7 +11,7 @@ type InviteUserCommand = {
lastName: string;
password?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export type RegisterUserResponse = {

View File

@@ -25,7 +25,7 @@ import { createSessionAndUpdateCookie } from "./cookie";
export type SendLoginnameCommand = {
loginName: string;
authRequestId?: string;
requestId?: string;
organization?: string;
suffix?: string;
};
@@ -96,8 +96,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const params = new URLSearchParams();
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.organization) {
@@ -161,8 +161,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const params = new URLSearchParams({ userId });
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.organization) {
@@ -241,8 +241,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
command.requestId,
);
if (!session.factors?.user?.id) {
@@ -267,7 +266,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
session,
humanUser,
session.factors.user.organizationId,
command.authRequestId,
command.requestId,
);
if (inviteCheck?.redirect) {
@@ -286,8 +285,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
);
}
if (command.authRequestId) {
paramsAuthenticatorSetup.append("authRequestId", command.authRequestId);
if (command.requestId) {
paramsAuthenticatorSetup.append("requestId", command.requestId);
}
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
@@ -315,8 +314,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
command.organization ?? session.factors?.user?.organizationId;
}
if (command.authRequestId) {
paramsPassword.authRequestId = command.authRequestId;
if (command.requestId) {
paramsPassword.requestId = command.requestId;
}
return {
@@ -332,8 +331,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
}
const paramsPasskey: any = { loginName: command.loginName };
if (command.authRequestId) {
paramsPasskey.authRequestId = command.authRequestId;
if (command.requestId) {
paramsPasskey.requestId = command.requestId;
}
if (command.organization || session.factors?.user?.organizationId) {
@@ -351,8 +350,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option
};
if (command.authRequestId) {
passkeyParams.authRequestId = command.authRequestId;
if (command.requestId) {
passkeyParams.requestId = command.requestId;
}
if (command.organization || session.factors?.user?.organizationId) {
@@ -371,8 +370,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// 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.requestId) {
paramsPasswordDefault.requestId = command.requestId;
}
if (command.organization || session.factors?.user?.organizationId) {
@@ -435,8 +434,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) {
const params = new URLSearchParams({ organization: orgToRegisterOn });
if (command.authRequestId) {
params.set("authRequestId", command.authRequestId);
if (command.requestId) {
params.set("requestId", command.requestId);
}
if (command.loginName) {
@@ -452,8 +451,8 @@ export async function sendLoginname(command: SendLoginnameCommand) {
loginName: command.loginName,
});
if (command.authRequestId) {
paramsPasswordDefault.append("authRequestId", command.authRequestId);
if (command.requestId) {
paramsPasswordDefault.append("requestId", command.requestId);
}
if (command.organization) {

View File

@@ -20,7 +20,7 @@ export type SetOTPCommand = {
loginName?: string;
sessionId?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
code: string;
method: string;
};
@@ -72,7 +72,7 @@ export async function setOTP(command: SetOTPCommand) {
recentSession,
checks,
undefined,
command.authRequestId,
command.requestId,
loginSettings?.secondFactorCheckLifetime,
).then((session) => {
return {

View File

@@ -139,12 +139,12 @@ type SendPasskeyCommand = {
sessionId?: string;
organization?: string;
checks?: Checks;
authRequestId?: string;
requestId?: string;
lifetime?: Duration;
};
export async function sendPasskey(command: SendPasskeyCommand) {
let { loginName, sessionId, organization, checks, authRequestId } = command;
let { loginName, sessionId, organization, checks, requestId } = command;
const recentSession = sessionId
? await getSessionCookieById({ sessionId })
: loginName
@@ -176,7 +176,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
recentSession,
checks,
undefined,
authRequestId,
requestId,
lifetime,
);
@@ -203,7 +203,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
session,
humanUser,
organization,
authRequestId,
requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -211,11 +211,11 @@ export async function sendPasskey(command: SendPasskeyCommand) {
}
const url =
authRequestId && session.id
requestId && session.id
? await getNextUrl(
{
sessionId: session.id,
authRequestId: authRequestId,
requestId: requestId,
organization: organization,
},
loginSettings?.defaultRedirectUri,

View File

@@ -42,7 +42,7 @@ import {
type ResetPasswordCommand = {
loginName: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export async function resetPassword(command: ResetPasswordCommand) {
@@ -76,7 +76,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
userId,
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.authRequestId ? `&authRequestId=${command.authRequestId}` : ""),
(command.requestId ? `&requestId=${command.requestId}` : ""),
});
}
@@ -84,7 +84,7 @@ export type UpdateSessionCommand = {
loginName: string;
organization?: string;
checks: Checks;
authRequestId?: string;
requestId?: string;
};
export async function sendPassword(command: UpdateSessionCommand) {
@@ -127,8 +127,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
try {
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
command.requestId,
loginSettings?.passwordCheckLifetime,
);
} catch (error: any) {
@@ -160,7 +159,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
sessionCookie,
command.checks,
undefined,
command.authRequestId,
command.requestId,
loginSettings?.passwordCheckLifetime,
);
} catch (error: any) {
@@ -227,7 +226,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
session,
humanUser,
command.organization,
command.authRequestId,
command.requestId,
);
if (passwordChangedCheck?.redirect) {
@@ -244,7 +243,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
session,
humanUser,
command.organization,
command.authRequestId,
command.requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -273,18 +272,18 @@ export async function sendPassword(command: UpdateSessionCommand) {
loginSettings,
authMethods,
command.organization,
command.authRequestId,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
return mfaFactorCheck;
}
if (command.authRequestId && session.id) {
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},

View File

@@ -19,7 +19,7 @@ type RegisterUserCommand = {
lastName: string;
password?: string;
organization?: string;
authRequestId?: string;
requestId?: string;
};
export type RegisterUserResponse = {
@@ -71,8 +71,7 @@ export async function registerUser(command: RegisterUserCommand) {
const session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
command.requestId,
command.password ? loginSettings?.passwordCheckLifetime : undefined,
);
@@ -86,8 +85,8 @@ export async function registerUser(command: RegisterUserCommand) {
organization: session.factors.user.organizationId,
});
if (command.authRequestId) {
params.append("authRequestId", command.authRequestId);
if (command.requestId) {
params.append("requestId", command.requestId);
}
return { redirect: "/passkey/set?" + params };
@@ -111,7 +110,7 @@ export async function registerUser(command: RegisterUserCommand) {
session,
humanUser,
session.factors.user.organizationId,
command.authRequestId,
command.requestId,
);
if (emailVerificationCheck?.redirect) {
@@ -119,10 +118,10 @@ export async function registerUser(command: RegisterUserCommand) {
}
const url = await getNextUrl(
command.authRequestId && session.id
command.requestId && session.id
? {
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization: session.factors.user.organizationId,
}
: {

View File

@@ -21,9 +21,9 @@ import {
import { getServiceUrlFromHeaders } from "../service";
export async function continueWithSession({
authRequestId,
requestId,
...session
}: Session & { authRequestId?: string }) {
}: Session & { requestId?: string }) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
@@ -34,11 +34,11 @@ export async function continueWithSession({
});
const url =
authRequestId && session.id && session.factors?.user
requestId && session.id && session.factors?.user
? await getNextUrl(
{
sessionId: session.id,
authRequestId: authRequestId,
requestId: requestId,
organization: session.factors.user.organizationId,
},
loginSettings?.defaultRedirectUri,
@@ -62,20 +62,14 @@ export type UpdateSessionCommand = {
sessionId?: string;
organization?: string;
checks?: Checks;
authRequestId?: string;
requestId?: string;
challenges?: RequestChallenges;
lifetime?: Duration;
};
export async function updateSession(options: UpdateSessionCommand) {
let {
loginName,
sessionId,
organization,
checks,
authRequestId,
challenges,
} = options;
let { loginName, sessionId, organization, checks, requestId, challenges } =
options;
const recentSession = sessionId
? await getSessionCookieById({ sessionId })
: loginName
@@ -123,7 +117,7 @@ export async function updateSession(options: UpdateSessionCommand) {
recentSession,
checks,
challenges,
authRequestId,
requestId,
lifetime,
);

View File

@@ -59,7 +59,7 @@ type VerifyUserByEmailCommand = {
organization?: string;
code: string;
isInvite: boolean;
authRequestId?: string;
requestId?: string;
};
export async function sendVerification(command: VerifyUserByEmailCommand) {
@@ -155,11 +155,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
},
});
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
);
session = await createSessionAndUpdateCookie(checks, command.requestId);
}
if (!session?.factors?.user?.id) {
@@ -212,7 +208,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
loginSettings,
authMethodResponse.authMethodTypes,
command.organization,
command.authRequestId,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
@@ -220,11 +216,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
}
// login user if no additional steps are required
if (command.authRequestId && session.id) {
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},
@@ -248,7 +244,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
type resendVerifyEmailCommand = {
userId: string;
isInvite: boolean;
authRequestId?: string;
requestId?: string;
};
export async function resendVerification(command: resendVerifyEmailCommand) {
@@ -268,9 +264,7 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
urlTemplate:
`${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.authRequestId
? `&authRequestId=${command.authRequestId}`
: ""),
(command.requestId ? `&requestId=${command.requestId}` : ""),
});
}
@@ -291,7 +285,7 @@ export async function sendEmailCode(command: sendEmailCommand) {
export type SendVerificationRedirectWithoutCheckCommand = {
organization?: string;
authRequestId?: string;
requestId?: string;
} & (
| { userId: string; loginName?: never }
| { userId?: never; loginName: string }
@@ -370,11 +364,7 @@ export async function sendVerificationRedirectWithoutCheck(
},
});
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
);
session = await createSessionAndUpdateCookie(checks, command.requestId);
}
if (!session?.factors?.user?.id) {
@@ -427,7 +417,7 @@ export async function sendVerificationRedirectWithoutCheck(
loginSettings,
authMethodResponse.authMethodTypes,
command.organization,
command.authRequestId,
command.requestId,
);
if (mfaFactorCheck?.redirect) {
@@ -435,11 +425,11 @@ export async function sendVerificationRedirectWithoutCheck(
}
// login user if no additional steps are required
if (command.authRequestId && session.id) {
if (command.requestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
requestId: command.requestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},

View File

@@ -3,6 +3,7 @@ import { createServerTransport } from "@zitadel/client/node";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
@@ -15,7 +16,8 @@ type ServiceClass =
| typeof OrganizationService
| typeof SessionService
| typeof OIDCService
| typeof SettingsService;
| typeof SettingsService
| typeof SAMLService;
export async function createServiceForHost<T extends ServiceClass>(
service: T,

View File

@@ -1,7 +1,15 @@
import { timestampDate } from "@zitadel/client";
import { AuthRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { SAMLRequest } from "@zitadel/proto/zitadel/saml/v2/authorization_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getMostRecentCookieWithLoginname } from "./cookies";
import { getSession } from "./zitadel";
import {
getLoginSettings,
getSession,
listAuthenticationMethodTypes,
} from "./zitadel";
type LoadMostRecentSessionParams = {
serviceUrl: string;
@@ -29,3 +37,160 @@ export async function loadMostRecentSession({
sessionToken: recent.token,
}).then((resp: GetSessionResponse) => resp.session);
}
/**
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
**/
export async function isSessionValid({
serviceUrl,
session,
}: {
serviceUrl: string;
session: Session;
}): Promise<boolean> {
// session can't be checked without user
if (!session.factors?.user) {
console.warn("Session has no user");
return false;
}
let mfaValid = true;
const authMethodTypes = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const authMethods = authMethodTypes.authMethodTypes;
if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) {
mfaValid = !!session.factors.totp?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid totpEmail factor",
session.factors.totp?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_EMAIL)
) {
mfaValid = !!session.factors.otpEmail?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpEmail factor",
session.factors.otpEmail?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.OTP_SMS)
) {
mfaValid = !!session.factors.otpSms?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid otpSms factor",
session.factors.otpSms?.verifiedAt,
);
}
} else if (
authMethods &&
authMethods.includes(AuthenticationMethodType.U2F)
) {
mfaValid = !!session.factors.webAuthN?.verifiedAt;
if (!mfaValid) {
console.warn(
"Session has no valid u2f factor",
session.factors.webAuthN?.verifiedAt,
);
}
} else {
// only check settings if no auth methods are available, as this would require a setup
const loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors?.user?.organizationId,
});
if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) {
const otpEmail = session.factors.otpEmail?.verifiedAt;
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 || idp);
if (!mfaValid) {
console.warn("Session has no valid multifactor", session.factors);
}
} else {
mfaValid = true;
}
}
const validPassword = session?.factors?.password?.verifiedAt;
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validIDP = session?.factors?.intent?.verifiedAt;
const stillValid = session.expirationDate
? timestampDate(session.expirationDate).getTime() > new Date().getTime()
: true;
if (!stillValid) {
console.warn(
"Session is expired",
session.expirationDate
? timestampDate(session.expirationDate).toDateString()
: "no expiration date",
);
}
const validChecks = !!(validPassword || validPasskey || validIDP);
return stillValid && validChecks && mfaValid;
}
export async function findValidSession({
serviceUrl,
sessions,
authRequest,
samlRequest,
}: {
serviceUrl: string;
sessions: Session[];
authRequest?: AuthRequest;
samlRequest?: SAMLRequest;
}): Promise<Session | undefined> {
const sessionsWithHint = sessions.filter((s) => {
if (authRequest && authRequest.hintUserId) {
return s.factors?.user?.id === authRequest.hintUserId;
}
if (authRequest && authRequest.loginHint) {
return s.factors?.user?.loginName === authRequest.loginHint;
}
if (samlRequest) {
// TODO: do whatever
return true;
}
return true;
});
if (sessionsWithHint.length === 0) {
return undefined;
}
// sort by change date descending
sessionsWithHint.sort((a, b) => {
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
return dateB - dateA;
});
// return the first valid session according to settings
for (const session of sessionsWithHint) {
if (await isSessionValid({ serviceUrl, session })) {
return session;
}
}
return undefined;
}

View File

@@ -11,7 +11,7 @@ export function checkPasswordChangeRequired(
session: Session,
humanUser: HumanUser | undefined,
organization?: string,
authRequestId?: string,
requestId?: string,
) {
let isOutdated = false;
if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) {
@@ -35,8 +35,8 @@ export function checkPasswordChangeRequired(
);
}
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
return { redirect: "/password/change?" + params };
@@ -47,7 +47,7 @@ export function checkInvite(
session: Session,
humanUser?: HumanUser,
organization?: string,
authRequestId?: string,
requestId?: string,
) {
if (!humanUser?.email?.isVerified) {
const paramsVerify = new URLSearchParams({
@@ -63,8 +63,8 @@ export function checkInvite(
);
}
if (authRequestId) {
paramsVerify.append("authRequestId", authRequestId);
if (requestId) {
paramsVerify.append("requestId", requestId);
}
return { redirect: "/verify?" + paramsVerify };
@@ -75,7 +75,7 @@ export function checkEmailVerification(
session: Session,
humanUser?: HumanUser,
organization?: string,
authRequestId?: string,
requestId?: string,
) {
if (
!humanUser?.email?.isVerified &&
@@ -85,8 +85,8 @@ export function checkEmailVerification(
loginName: session.factors?.user?.loginName as string,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -105,7 +105,7 @@ export function checkMFAFactors(
loginSettings: LoginSettings | undefined,
authMethods: AuthenticationMethodType[],
organization?: string,
authRequestId?: string,
requestId?: string,
) {
const availableMultiFactors = authMethods?.filter(
(m: AuthenticationMethodType) =>
@@ -128,8 +128,8 @@ export function checkMFAFactors(
loginName: session.factors?.user?.loginName as string,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -155,8 +155,8 @@ export function checkMFAFactors(
loginName: session.factors?.user?.loginName as string,
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -177,8 +177,8 @@ export function checkMFAFactors(
checkAfter: "true", // this defines if the check is directly made after the setup
});
if (authRequestId) {
params.append("authRequestId", authRequestId);
if (requestId) {
params.append("requestId", requestId);
}
if (organization || session.factors?.user?.organizationId) {
@@ -205,8 +205,8 @@ export function checkMFAFactors(
// prompt: "true",
// });
// if (authRequestId) {
// params.append("authRequestId", authRequestId);
// if (requestId) {
// params.append("requestId", requestId);
// }
// if (organization) {

View File

@@ -8,6 +8,10 @@ import {
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
import {
CreateResponseRequest,
SAMLService,
} from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import {
Checks,
@@ -55,11 +59,9 @@ async function cacheWrapper<T>(callback: Promise<T>) {
export async function getBrandingSettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -74,11 +76,9 @@ export async function getBrandingSettings({
export async function getLoginSettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -93,11 +93,9 @@ export async function getLoginSettings({
export async function getLockoutSettings({
serviceUrl,
orgId,
}: {
serviceUrl: string;
orgId?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -112,11 +110,9 @@ export async function getLockoutSettings({
export async function getPasswordExpirySettings({
serviceUrl,
orgId,
}: {
serviceUrl: string;
orgId?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -131,11 +127,9 @@ export async function getPasswordExpirySettings({
export async function listIDPLinks({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -148,11 +142,9 @@ export async function listIDPLinks({
export async function addOTPEmail({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -165,11 +157,9 @@ export async function addOTPEmail({
export async function addOTPSMS({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -182,11 +172,9 @@ export async function addOTPSMS({
export async function registerTOTP({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -214,11 +202,9 @@ export async function getGeneralSettings({
export async function getLegalAndSupportSettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -233,11 +219,9 @@ export async function getLegalAndSupportSettings({
export async function getPasswordComplexitySettings({
serviceUrl,
organization,
}: {
serviceUrl: string;
organization?: string;
}) {
const settingsService: Client<typeof SettingsService> =
@@ -252,32 +236,26 @@ export async function getPasswordComplexitySettings({
export async function createSessionFromChecks({
serviceUrl,
checks,
challenges,
lifetime,
}: {
serviceUrl: string;
checks: Checks;
challenges: RequestChallenges | undefined;
lifetime?: Duration;
}) {
const sessionService: Client<typeof SessionService> =
await createServiceForHost(SessionService, serviceUrl);
return sessionService.createSession({ checks, challenges, lifetime }, {});
return sessionService.createSession({ checks, lifetime }, {});
}
export async function createSessionForUserIdAndIdpIntent({
serviceUrl,
userId,
idpIntent,
lifetime,
}: {
serviceUrl: string;
userId: string;
idpIntent: {
idpIntentId?: string | undefined;
@@ -304,7 +282,6 @@ export async function createSessionForUserIdAndIdpIntent({
export async function setSession({
serviceUrl,
sessionId,
sessionToken,
challenges,
@@ -312,7 +289,6 @@ export async function setSession({
lifetime,
}: {
serviceUrl: string;
sessionId: string;
sessionToken: string;
challenges: RequestChallenges | undefined;
@@ -337,12 +313,10 @@ export async function setSession({
export async function getSession({
serviceUrl,
sessionId,
sessionToken,
}: {
serviceUrl: string;
sessionId: string;
sessionToken: string;
}) {
@@ -354,12 +328,10 @@ export async function getSession({
export async function deleteSession({
serviceUrl,
sessionId,
sessionToken,
}: {
serviceUrl: string;
sessionId: string;
sessionToken: string;
}) {
@@ -371,7 +343,6 @@ export async function deleteSession({
type ListSessionsCommand = {
serviceUrl: string;
ids: string[];
};
@@ -400,7 +371,6 @@ export async function listSessions({
export type AddHumanUserData = {
serviceUrl: string;
firstName: string;
lastName: string;
email: string;
@@ -410,7 +380,6 @@ export type AddHumanUserData = {
export async function addHumanUser({
serviceUrl,
email,
firstName,
lastName,
@@ -443,11 +412,9 @@ export async function addHumanUser({
export async function addHuman({
serviceUrl,
request,
}: {
serviceUrl: string;
request: AddHumanUserRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -460,12 +427,10 @@ export async function addHuman({
export async function verifyTOTPRegistration({
serviceUrl,
code,
userId,
}: {
serviceUrl: string;
code: string;
userId: string;
}) {
@@ -479,11 +444,9 @@ export async function verifyTOTPRegistration({
export async function getUserByID({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -496,12 +459,10 @@ export async function getUserByID({
export async function verifyInviteCode({
serviceUrl,
userId,
verificationCode,
}: {
serviceUrl: string;
userId: string;
verificationCode: string;
}) {
@@ -515,11 +476,9 @@ export async function verifyInviteCode({
export async function resendInviteCode({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -532,12 +491,10 @@ export async function resendInviteCode({
export async function sendEmailCode({
serviceUrl,
userId,
urlTemplate,
}: {
serviceUrl: string;
userId: string;
urlTemplate: string;
}) {
@@ -563,12 +520,10 @@ export async function sendEmailCode({
export async function createInviteCode({
serviceUrl,
urlTemplate,
userId,
}: {
serviceUrl: string;
urlTemplate: string;
userId: string;
}) {
@@ -600,7 +555,6 @@ export async function createInviteCode({
export type ListUsersCommand = {
serviceUrl: string;
loginName?: string;
userName?: string;
email?: string;
@@ -610,7 +564,6 @@ export type ListUsersCommand = {
export async function listUsers({
serviceUrl,
loginName,
userName,
phone,
@@ -709,7 +662,6 @@ export async function listUsers({
export type SearchUsersCommand = {
serviceUrl: string;
searchValue: string;
loginSettings: LoginSettings;
organizationId?: string;
@@ -755,7 +707,6 @@ const EmailQuery = (searchValue: string) =>
* */
export async function searchUsers({
serviceUrl,
searchValue,
loginSettings,
organizationId,
@@ -900,11 +851,9 @@ export async function getDefaultOrg({
export async function getOrgsByDomain({
serviceUrl,
domain,
}: {
serviceUrl: string;
domain: string;
}) {
const orgService: Client<typeof OrganizationService> =
@@ -927,7 +876,6 @@ export async function getOrgsByDomain({
export async function startIdentityProviderFlow({
serviceUrl,
idpId,
urls,
}: {
@@ -952,7 +900,6 @@ export async function startIdentityProviderFlow({
export async function retrieveIdentityProviderInformation({
serviceUrl,
idpIntentId,
idpIntentToken,
}: {
@@ -974,11 +921,9 @@ export async function retrieveIdentityProviderInformation({
export async function getAuthRequest({
serviceUrl,
authRequestId,
}: {
serviceUrl: string;
authRequestId: string;
}) {
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
@@ -990,11 +935,9 @@ export async function getAuthRequest({
export async function createCallback({
serviceUrl,
req,
}: {
serviceUrl: string;
req: CreateCallbackRequest;
}) {
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
@@ -1002,14 +945,38 @@ export async function createCallback({
return oidcService.createCallback(req);
}
export async function getSAMLRequest({
serviceUrl,
samlRequestId,
}: {
serviceUrl: string;
samlRequestId: string;
}) {
const samlService = await createServiceForHost(SAMLService, serviceUrl);
return samlService.getSAMLRequest({
samlRequestId,
});
}
export async function createResponse({
serviceUrl,
req,
}: {
serviceUrl: string;
req: CreateResponseRequest;
}) {
const samlService = await createServiceForHost(SAMLService, serviceUrl);
return samlService.createResponse(req);
}
export async function verifyEmail({
serviceUrl,
userId,
verificationCode,
}: {
serviceUrl: string;
userId: string;
verificationCode: string;
}) {
@@ -1029,12 +996,10 @@ export async function verifyEmail({
export async function resendEmailCode({
serviceUrl,
userId,
urlTemplate,
}: {
serviceUrl: string;
userId: string;
urlTemplate: string;
}) {
@@ -1058,12 +1023,10 @@ export async function resendEmailCode({
export async function retrieveIDPIntent({
serviceUrl,
id,
token,
}: {
serviceUrl: string;
id: string;
token: string;
}) {
@@ -1080,11 +1043,9 @@ export async function retrieveIDPIntent({
export async function getIDPByID({
serviceUrl,
id,
}: {
serviceUrl: string;
id: string;
}) {
const idpService: Client<typeof IdentityProviderService> =
@@ -1095,12 +1056,10 @@ export async function getIDPByID({
export async function addIDPLink({
serviceUrl,
idp,
userId,
}: {
serviceUrl: string;
idp: { id: string; userId: string; userName: string };
userId: string;
}) {
@@ -1124,12 +1083,10 @@ export async function addIDPLink({
export async function passwordReset({
serviceUrl,
userId,
urlTemplate,
}: {
serviceUrl: string;
userId: string;
urlTemplate?: string;
}) {
@@ -1161,14 +1118,12 @@ export async function passwordReset({
export async function setUserPassword({
serviceUrl,
userId,
password,
user,
code,
}: {
serviceUrl: string;
userId: string;
password: string;
user: User;
@@ -1224,11 +1179,9 @@ export async function setUserPassword({
export async function setPassword({
serviceUrl,
payload,
}: {
serviceUrl: string;
payload: SetPasswordRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1247,11 +1200,9 @@ export async function setPassword({
*/
export async function createPasskeyRegistrationLink({
serviceUrl,
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1277,12 +1228,10 @@ export async function createPasskeyRegistrationLink({
*/
export async function registerU2F({
serviceUrl,
userId,
domain,
}: {
serviceUrl: string;
userId: string;
domain: string;
}) {
@@ -1305,11 +1254,9 @@ export async function registerU2F({
*/
export async function verifyU2FRegistration({
serviceUrl,
request,
}: {
serviceUrl: string;
request: VerifyU2FRegistrationRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1329,12 +1276,10 @@ export async function verifyU2FRegistration({
*/
export async function getActiveIdentityProviders({
serviceUrl,
orgId,
linking_allowed,
}: {
serviceUrl: string;
orgId?: string;
linking_allowed?: boolean;
}) {
@@ -1356,11 +1301,9 @@ export async function getActiveIdentityProviders({
*/
export async function verifyPasskeyRegistration({
serviceUrl,
request,
}: {
serviceUrl: string;
request: VerifyPasskeyRegistrationRequest;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
@@ -1381,13 +1324,11 @@ export async function verifyPasskeyRegistration({
*/
export async function registerPasskey({
serviceUrl,
userId,
code,
domain,
}: {
serviceUrl: string;
userId: string;
code: { id: string; code: string };
domain: string;
@@ -1415,7 +1356,6 @@ export async function listAuthenticationMethodTypes({
userId,
}: {
serviceUrl: string;
userId: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(

View File

@@ -4,6 +4,7 @@ import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_servi
import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js";
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb.js";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb.js";
import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb.js";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb.js";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb.js";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb.js";
@@ -14,6 +15,7 @@ export const createUserServiceClient = createClientFor(UserService);
export const createSettingsServiceClient = createClientFor(SettingsService);
export const createSessionServiceClient = createClientFor(SessionService);
export const createOIDCServiceClient = createClientFor(OIDCService);
export const createSAMLServiceClient = createClientFor(SAMLService);
export const createOrganizationServiceClient = createClientFor(OrganizationService);
export const createFeatureServiceClient = createClientFor(FeatureService);
export const createIdpServiceClient = createClientFor(IdentityProviderService);