mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-14 10:42:13 +00:00
check if session is valid according to loginsettings (forceMFA)
This commit is contained in:
@@ -394,3 +394,4 @@ Timebased features like the multifactor init prompt or password expiry, are not
|
|||||||
- Lockout Settings
|
- Lockout Settings
|
||||||
- Password Expiry Settings
|
- Password Expiry Settings
|
||||||
- Login Settings: multifactor init prompt
|
- Login Settings: multifactor init prompt
|
||||||
|
- forceMFA on login settings is not checked for IDPs
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
createCallback,
|
createCallback,
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getAuthRequest,
|
getAuthRequest,
|
||||||
|
getLoginSettings,
|
||||||
getOrgsByDomain,
|
getOrgsByDomain,
|
||||||
listSessions,
|
listSessions,
|
||||||
startIdentityProviderFlow,
|
startIdentityProviderFlow,
|
||||||
@@ -37,7 +38,32 @@ 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:(.+)/;
|
||||||
|
|
||||||
function isSessionValid(session: Session): boolean {
|
/**
|
||||||
|
* 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(
|
||||||
|
session: Session,
|
||||||
|
checkLoginSettings?: boolean,
|
||||||
|
): Promise<boolean> {
|
||||||
|
let mfaValid = true;
|
||||||
|
if (checkLoginSettings && session.factors?.user?.organizationId) {
|
||||||
|
const loginSettings = await getLoginSettings(
|
||||||
|
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;
|
||||||
|
|
||||||
|
// must have one single check
|
||||||
|
mfaValid = !!(otpEmail || otpSms || totp || webAuthN);
|
||||||
|
} else {
|
||||||
|
mfaValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const validPassword = session?.factors?.password?.verifiedAt;
|
const validPassword = session?.factors?.password?.verifiedAt;
|
||||||
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
|
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
|
||||||
const validIDP = session?.factors?.intent?.verifiedAt;
|
const validIDP = session?.factors?.intent?.verifiedAt;
|
||||||
@@ -46,20 +72,16 @@ function isSessionValid(session: Session): boolean {
|
|||||||
? timestampDate(session.expirationDate) > new Date()
|
? timestampDate(session.expirationDate) > new Date()
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const validFactors = !!(
|
const validFactors = !!(validPassword || validPasskey || validIDP);
|
||||||
(validPassword || validPasskey || validIDP) &&
|
|
||||||
stillValid
|
|
||||||
);
|
|
||||||
|
|
||||||
return stillValid && validFactors;
|
return stillValid && validFactors && mfaValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findValidSession(
|
async function findValidSession(
|
||||||
sessions: Session[],
|
sessions: Session[],
|
||||||
authRequest: AuthRequest,
|
authRequest: AuthRequest,
|
||||||
): Session | undefined {
|
): Promise<Session | undefined> {
|
||||||
const validSessionsWithHint = sessions
|
const sessionsWithHint = sessions.filter((s) => {
|
||||||
.filter((s) => {
|
|
||||||
if (authRequest.hintUserId) {
|
if (authRequest.hintUserId) {
|
||||||
return s.factors?.user?.id === authRequest.hintUserId;
|
return s.factors?.user?.id === authRequest.hintUserId;
|
||||||
}
|
}
|
||||||
@@ -67,22 +89,27 @@ function findValidSession(
|
|||||||
return s.factors?.user?.loginName === authRequest.loginHint;
|
return s.factors?.user?.loginName === authRequest.loginHint;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
});
|
||||||
.filter(isSessionValid);
|
|
||||||
|
|
||||||
if (validSessionsWithHint.length === 0) {
|
if (sessionsWithHint.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort by change date descending
|
// sort by change date descending
|
||||||
validSessionsWithHint.sort((a, b) => {
|
sessionsWithHint.sort((a, b) => {
|
||||||
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
|
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
|
||||||
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
|
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
|
||||||
return dateB - dateA;
|
return dateB - dateA;
|
||||||
});
|
});
|
||||||
|
|
||||||
// return most recently changed session
|
// return the first valid session according to settings
|
||||||
return sessions[0];
|
for (const session of sessionsWithHint) {
|
||||||
|
if (await isSessionValid(session, true)) {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@@ -103,22 +130,19 @@ export async function GET(request: NextRequest) {
|
|||||||
sessions = await loadSessions(ids);
|
sessions = await loadSessions(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: before automatically redirecting to the callbackUrl, check if the session is still valid
|
|
||||||
* possible scenaio:
|
|
||||||
* 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);
|
|
||||||
**/
|
|
||||||
|
|
||||||
if (authRequestId && sessionId) {
|
if (authRequestId && sessionId) {
|
||||||
console.log(
|
console.log(
|
||||||
`Login with session: ${sessionId} and authRequest: ${authRequestId}`,
|
`Login with session: ${sessionId} and authRequest: ${authRequestId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
let selectedSession = sessions.find((s) => s.id === sessionId);
|
const selectedSession = sessions.find((s) => s.id === sessionId);
|
||||||
|
|
||||||
if (selectedSession && selectedSession.id) {
|
if (selectedSession && selectedSession.id) {
|
||||||
console.log(`Found session ${selectedSession.id}`);
|
console.log(`Found session ${selectedSession.id}`);
|
||||||
|
|
||||||
|
const isValid = await isSessionValid(selectedSession, true);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
const cookie = sessionCookies.find(
|
const cookie = sessionCookies.find(
|
||||||
(cookie) => cookie.id === selectedSession?.id,
|
(cookie) => cookie.id === selectedSession?.id,
|
||||||
);
|
);
|
||||||
@@ -154,6 +178,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (authRequestId) {
|
if (authRequestId) {
|
||||||
const { authRequest } = await getAuthRequest({ authRequestId });
|
const { authRequest } = await getAuthRequest({ authRequestId });
|
||||||
@@ -314,7 +339,7 @@ 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 = findValidSession(sessions, authRequest);
|
const selectedSession = await findValidSession(sessions, authRequest);
|
||||||
|
|
||||||
if (!selectedSession || !selectedSession.id) {
|
if (!selectedSession || !selectedSession.id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -351,7 +376,7 @@ 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 = findValidSession(sessions, authRequest);
|
let selectedSession = await findValidSession(sessions, authRequest);
|
||||||
|
|
||||||
if (!selectedSession || !selectedSession.id) {
|
if (!selectedSession || !selectedSession.id) {
|
||||||
return gotoAccounts();
|
return gotoAccounts();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
|
|||||||
: 0;
|
: 0;
|
||||||
return dateB - dateA;
|
return dateB - dateA;
|
||||||
})
|
})
|
||||||
|
// TODO: add sorting to move invalid sessions to the bottom
|
||||||
.map((session, index) => {
|
.map((session, index) => {
|
||||||
return (
|
return (
|
||||||
<SessionItem
|
<SessionItem
|
||||||
|
|||||||
Reference in New Issue
Block a user