mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 09:54:00 +00:00
helpers
This commit is contained in:
@@ -1,18 +1,19 @@
|
|||||||
import { getAllSessions } from "@/lib/cookies";
|
import { getAllSessions } from "@/lib/cookies";
|
||||||
import { idpTypeToSlug } from "@/lib/idp";
|
import { idpTypeToSlug } from "@/lib/idp";
|
||||||
|
import { loginWithOIDCandSession } from "@/lib/oidc";
|
||||||
|
import { loginWithSAMLandSession } from "@/lib/saml";
|
||||||
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service";
|
||||||
|
import { findValidSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
createCallback,
|
createCallback,
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getAuthRequest,
|
getAuthRequest,
|
||||||
getLoginSettings,
|
|
||||||
getOrgsByDomain,
|
getOrgsByDomain,
|
||||||
listAuthenticationMethodTypes,
|
|
||||||
listSessions,
|
listSessions,
|
||||||
startIdentityProviderFlow,
|
startIdentityProviderFlow,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create, timestampDate } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import {
|
import {
|
||||||
AuthRequest,
|
AuthRequest,
|
||||||
Prompt,
|
Prompt,
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
SessionSchema,
|
SessionSchema,
|
||||||
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_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 { headers } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
@@ -30,6 +30,29 @@ export const dynamic = "force-dynamic";
|
|||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
export const fetchCache = "default-no-store";
|
export const fetchCache = "default-no-store";
|
||||||
|
|
||||||
|
const gotoAccounts = ({
|
||||||
|
request,
|
||||||
|
authRequest,
|
||||||
|
organization,
|
||||||
|
idPrefix,
|
||||||
|
}: {
|
||||||
|
request: NextRequest;
|
||||||
|
authRequest: AuthRequest;
|
||||||
|
organization: string;
|
||||||
|
idPrefix: string;
|
||||||
|
}): NextResponse<unknown> => {
|
||||||
|
const accountsUrl = new URL("/accounts", request.url);
|
||||||
|
|
||||||
|
if (authRequest?.id) {
|
||||||
|
accountsUrl.searchParams.set("requestId", `${idPrefix}${authRequest.id}`);
|
||||||
|
}
|
||||||
|
if (organization) {
|
||||||
|
accountsUrl.searchParams.set("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(accountsUrl);
|
||||||
|
};
|
||||||
|
|
||||||
async function loadSessions({
|
async function loadSessions({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
@@ -52,162 +75,19 @@ 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 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:(.+)/;
|
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,
|
|
||||||
serviceRegion: 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,
|
|
||||||
serviceRegion,
|
|
||||||
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,
|
|
||||||
serviceRegion,
|
|
||||||
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,
|
|
||||||
serviceRegion: 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, serviceRegion, session)) {
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
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 _headers = await headers();
|
||||||
const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl, serviceRegion } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
|
||||||
|
const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request
|
||||||
|
const samlRequestId = searchParams.get("samlRequest"); // saml initiated request
|
||||||
|
|
||||||
|
const requestId = searchParams.get("requestId"); // internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_
|
||||||
|
|
||||||
|
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
|
// 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");
|
const _rsc = searchParams.get("_rsc");
|
||||||
if (_rsc) {
|
if (_rsc) {
|
||||||
@@ -221,119 +101,36 @@ export async function GET(request: NextRequest) {
|
|||||||
sessions = await loadSessions({ serviceUrl, serviceRegion, ids });
|
sessions = await loadSessions({ serviceUrl, serviceRegion, ids });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authRequestId && sessionId) {
|
if (requestId && sessionId) {
|
||||||
console.log(
|
if (requestId.startsWith("oidc_")) {
|
||||||
`Login with session: ${sessionId} and authRequest: ${authRequestId}`,
|
// this finishes the login process for OIDC
|
||||||
);
|
await loginWithOIDCandSession({
|
||||||
|
|
||||||
const selectedSession = sessions.find((s) => s.id === sessionId);
|
|
||||||
|
|
||||||
if (selectedSession && selectedSession.id) {
|
|
||||||
console.log(`Found session ${selectedSession.id}`);
|
|
||||||
|
|
||||||
const isValid = await isSessionValid(
|
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
selectedSession,
|
authRequest: requestId.replace("oidc_", ""),
|
||||||
);
|
sessionId,
|
||||||
|
sessions,
|
||||||
console.log("Session is valid:", isValid);
|
sessionCookies,
|
||||||
|
request,
|
||||||
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.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,
|
|
||||||
serviceRegion,
|
|
||||||
req: create(CreateCallbackRequestSchema, {
|
|
||||||
authRequestId,
|
|
||||||
callbackKind: {
|
|
||||||
case: "session",
|
|
||||||
value: create(SessionSchema, session),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (callbackUrl) {
|
} else if (requestId.startsWith("saml_")) {
|
||||||
return NextResponse.redirect(callbackUrl);
|
// this finishes the login process for SAML
|
||||||
} else {
|
await loginWithSAMLandSession({
|
||||||
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,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
organization: selectedSession.factors?.user?.organizationId,
|
samlRequest: requestId.replace("saml_", ""),
|
||||||
|
sessionId,
|
||||||
|
sessions,
|
||||||
|
sessionCookies,
|
||||||
|
request,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loginSettings?.defaultRedirectUri) {
|
|
||||||
return NextResponse.redirect(loginSettings.defaultRedirectUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const signedinUrl = new URL("/signedin", request.url);
|
if (requestId) {
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authRequestId) {
|
|
||||||
const { authRequest } = await getAuthRequest({
|
const { authRequest } = await getAuthRequest({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
authRequestId,
|
authRequestId: requestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
let organization = "";
|
let organization = "";
|
||||||
@@ -396,8 +193,8 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
if (authRequestId) {
|
if (requestId) {
|
||||||
params.set("authRequestId", authRequestId);
|
params.set("requestId", `oidc_${requestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
@@ -428,22 +225,10 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gotoAccounts = (): NextResponse<unknown> => {
|
|
||||||
const accountsUrl = new URL("/accounts", request.url);
|
|
||||||
if (authRequest?.id) {
|
|
||||||
accountsUrl.searchParams.set("authRequestId", authRequest?.id);
|
|
||||||
}
|
|
||||||
if (organization) {
|
|
||||||
accountsUrl.searchParams.set("organization", organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.redirect(accountsUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
|
if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) {
|
||||||
const registerUrl = new URL("/register", request.url);
|
const registerUrl = new URL("/register", request.url);
|
||||||
if (authRequest.id) {
|
if (authRequest.id) {
|
||||||
registerUrl.searchParams.set("authRequestId", authRequest.id);
|
registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`);
|
||||||
}
|
}
|
||||||
if (organization) {
|
if (organization) {
|
||||||
registerUrl.searchParams.set("organization", organization);
|
registerUrl.searchParams.set("organization", organization);
|
||||||
@@ -456,7 +241,12 @@ export async function GET(request: NextRequest) {
|
|||||||
if (authRequest && sessions.length) {
|
if (authRequest && sessions.length) {
|
||||||
// if some accounts are available for selection and select_account is set
|
// if some accounts are available for selection and select_account is set
|
||||||
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
|
if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) {
|
||||||
return gotoAccounts();
|
return gotoAccounts({
|
||||||
|
request,
|
||||||
|
authRequest,
|
||||||
|
organization,
|
||||||
|
idPrefix: "oidc_",
|
||||||
|
});
|
||||||
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
|
} 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
|
* The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated
|
||||||
@@ -487,7 +277,10 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const loginNameUrl = new URL("/loginname", request.url);
|
const loginNameUrl = new URL("/loginname", request.url);
|
||||||
if (authRequest.id) {
|
if (authRequest.id) {
|
||||||
loginNameUrl.searchParams.set("authRequestId", authRequest.id);
|
loginNameUrl.searchParams.set(
|
||||||
|
"requestId",
|
||||||
|
`oidc_${authRequest.id}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (authRequest.loginHint) {
|
if (authRequest.loginHint) {
|
||||||
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
|
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
|
||||||
@@ -505,12 +298,12 @@ export async function GET(request: NextRequest) {
|
|||||||
* This means that the user should not be prompted to enter their password again.
|
* 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
|
* 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,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
sessions,
|
sessions,
|
||||||
authRequest,
|
authRequest,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!selectedSession || !selectedSession.id) {
|
if (!selectedSession || !selectedSession.id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -539,7 +332,7 @@ export async function GET(request: NextRequest) {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
req: create(CreateCallbackRequestSchema, {
|
req: create(CreateCallbackRequestSchema, {
|
||||||
authRequestId,
|
authRequestId: requestId,
|
||||||
callbackKind: {
|
callbackKind: {
|
||||||
case: "session",
|
case: "session",
|
||||||
value: create(SessionSchema, session),
|
value: create(SessionSchema, session),
|
||||||
@@ -549,15 +342,20 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.redirect(callbackUrl);
|
return NextResponse.redirect(callbackUrl);
|
||||||
} else {
|
} else {
|
||||||
// check for loginHint, userId hint and valid sessions
|
// check for loginHint, userId hint and valid sessions
|
||||||
let selectedSession = await findValidSession(
|
let selectedSession = await findValidSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
sessions,
|
sessions,
|
||||||
authRequest,
|
authRequest,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (!selectedSession || !selectedSession.id) {
|
if (!selectedSession || !selectedSession.id) {
|
||||||
return gotoAccounts();
|
return gotoAccounts({
|
||||||
|
request,
|
||||||
|
authRequest,
|
||||||
|
organization,
|
||||||
|
idPrefix: "oidc_",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookie = sessionCookies.find(
|
const cookie = sessionCookies.find(
|
||||||
@@ -565,7 +363,12 @@ export async function GET(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!cookie || !cookie.id || !cookie.token) {
|
if (!cookie || !cookie.id || !cookie.token) {
|
||||||
return gotoAccounts();
|
return gotoAccounts({
|
||||||
|
request,
|
||||||
|
authRequest,
|
||||||
|
organization,
|
||||||
|
idPrefix: "oidc_",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
@@ -578,7 +381,7 @@ export async function GET(request: NextRequest) {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
req: create(CreateCallbackRequestSchema, {
|
req: create(CreateCallbackRequestSchema, {
|
||||||
authRequestId,
|
authRequestId: requestId,
|
||||||
callbackKind: {
|
callbackKind: {
|
||||||
case: "session",
|
case: "session",
|
||||||
value: create(SessionSchema, session),
|
value: create(SessionSchema, session),
|
||||||
@@ -591,17 +394,27 @@ export async function GET(request: NextRequest) {
|
|||||||
console.log(
|
console.log(
|
||||||
"could not create callback, redirect user to choose other account",
|
"could not create callback, redirect user to choose other account",
|
||||||
);
|
);
|
||||||
return gotoAccounts();
|
return gotoAccounts({
|
||||||
|
request,
|
||||||
|
authRequest,
|
||||||
|
organization,
|
||||||
|
idPrefix: "oidc_",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return gotoAccounts();
|
return gotoAccounts({
|
||||||
|
request,
|
||||||
|
authRequest,
|
||||||
|
organization,
|
||||||
|
idPrefix: "oidc_",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const loginNameUrl = new URL("/loginname", request.url);
|
const loginNameUrl = new URL("/loginname", request.url);
|
||||||
|
|
||||||
loginNameUrl.searchParams.set("authRequestId", authRequestId);
|
loginNameUrl.searchParams.set("requestId", `oidc_${requestId}`);
|
||||||
if (authRequest?.loginHint) {
|
if (authRequest?.loginHint) {
|
||||||
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
|
loginNameUrl.searchParams.set("loginName", authRequest.loginHint);
|
||||||
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
|
loginNameUrl.searchParams.set("submit", "true"); // autosubmit
|
||||||
@@ -615,8 +428,9 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "No authRequestId provided" },
|
{ error: "No authRequest nor samlRequest provided" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
apps/login/src/lib/oidc.ts
Normal file
136
apps/login/src/lib/oidc.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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;
|
||||||
|
serviceRegion: string;
|
||||||
|
authRequest: string;
|
||||||
|
sessionId: string;
|
||||||
|
sessions: Session[];
|
||||||
|
sessionCookies: Cookie[];
|
||||||
|
request: NextRequest;
|
||||||
|
};
|
||||||
|
export async function loginWithOIDCandSession({
|
||||||
|
serviceUrl,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
authRequestId: 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,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
serviceRegion,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
apps/login/src/lib/saml.ts
Normal file
134
apps/login/src/lib/saml.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
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;
|
||||||
|
serviceRegion: string;
|
||||||
|
samlRequest: string;
|
||||||
|
sessionId: string;
|
||||||
|
sessions: Session[];
|
||||||
|
sessionCookies: Cookie[];
|
||||||
|
request: NextRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loginWithSAMLandSession({
|
||||||
|
serviceUrl,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
authRequestId: 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,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
serviceRegion,
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { createServerTransport } from "@zitadel/client/node";
|
|||||||
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
|
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
|
||||||
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_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 { 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 { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
||||||
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
@@ -15,7 +16,8 @@ type ServiceClass =
|
|||||||
| typeof OrganizationService
|
| typeof OrganizationService
|
||||||
| typeof SessionService
|
| typeof SessionService
|
||||||
| typeof OIDCService
|
| typeof OIDCService
|
||||||
| typeof SettingsService;
|
| typeof SettingsService
|
||||||
|
| typeof SAMLService;
|
||||||
|
|
||||||
export async function createServiceForHost<T extends ServiceClass>(
|
export async function createServiceForHost<T extends ServiceClass>(
|
||||||
service: T,
|
service: T,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
|
import { timestampDate } from "@zitadel/client";
|
||||||
|
import { AuthRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_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 { getMostRecentCookieWithLoginname } from "./cookies";
|
||||||
import { getSession } from "./zitadel";
|
import {
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
|
} from "./zitadel";
|
||||||
|
|
||||||
type LoadMostRecentSessionParams = {
|
type LoadMostRecentSessionParams = {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
@@ -29,3 +36,160 @@ export async function loadMostRecentSession({
|
|||||||
sessionToken: recent.token,
|
sessionToken: recent.token,
|
||||||
}).then((resp: GetSessionResponse) => resp.session);
|
}).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,
|
||||||
|
serviceRegion,
|
||||||
|
session,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
serviceRegion: 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,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
serviceRegion,
|
||||||
|
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,
|
||||||
|
serviceRegion,
|
||||||
|
sessions,
|
||||||
|
authRequest,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
serviceRegion: 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, serviceRegion, session })) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import {
|
|||||||
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_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 { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
||||||
import {
|
import {
|
||||||
Checks,
|
Checks,
|
||||||
@@ -1030,6 +1034,24 @@ export async function createCallback({
|
|||||||
return oidcService.createCallback(req);
|
return oidcService.createCallback(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createResponse({
|
||||||
|
serviceUrl,
|
||||||
|
serviceRegion,
|
||||||
|
req,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
serviceRegion: string;
|
||||||
|
req: CreateResponseRequest;
|
||||||
|
}) {
|
||||||
|
const samlService = await createServiceForHost(
|
||||||
|
SAMLService,
|
||||||
|
serviceUrl,
|
||||||
|
serviceRegion,
|
||||||
|
);
|
||||||
|
|
||||||
|
return samlService.createResponse(req);
|
||||||
|
}
|
||||||
|
|
||||||
export async function verifyEmail({
|
export async function verifyEmail({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
serviceRegion,
|
serviceRegion,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_servi
|
|||||||
import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
|
||||||
import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_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 { 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 { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
||||||
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
@@ -14,6 +15,7 @@ export const createUserServiceClient = createClientFor(UserService);
|
|||||||
export const createSettingsServiceClient = createClientFor(SettingsService);
|
export const createSettingsServiceClient = createClientFor(SettingsService);
|
||||||
export const createSessionServiceClient = createClientFor(SessionService);
|
export const createSessionServiceClient = createClientFor(SessionService);
|
||||||
export const createOIDCServiceClient = createClientFor(OIDCService);
|
export const createOIDCServiceClient = createClientFor(OIDCService);
|
||||||
|
export const createSAMLServiceClient = createClientFor(SAMLService);
|
||||||
export const createOrganizationServiceClient = createClientFor(OrganizationService);
|
export const createOrganizationServiceClient = createClientFor(OrganizationService);
|
||||||
export const createFeatureServiceClient = createClientFor(FeatureService);
|
export const createFeatureServiceClient = createClientFor(FeatureService);
|
||||||
export const createIdpServiceClient = createClientFor(IdentityProviderService);
|
export const createIdpServiceClient = createClientFor(IdentityProviderService);
|
||||||
|
|||||||
Reference in New Issue
Block a user