Skip to content

Commit a4d474a

Browse files
committed
check if session is valid according to loginsettings (forceMFA)
1 parent ac08431 commit a4d474a

File tree

3 files changed

+87
-60
lines changed

3 files changed

+87
-60
lines changed

apps/login/readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,4 @@ Timebased features like the multifactor init prompt or password expiry, are not
394394
- Lockout Settings
395395
- Password Expiry Settings
396396
- Login Settings: multifactor init prompt
397+
- forceMFA on login settings is not checked for IDPs

apps/login/src/app/login/route.ts

Lines changed: 85 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createCallback,
66
getActiveIdentityProviders,
77
getAuthRequest,
8+
getLoginSettings,
89
getOrgsByDomain,
910
listSessions,
1011
startIdentityProviderFlow,
@@ -37,7 +38,32 @@ const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/;
3738
const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options
3839
const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/;
3940

40-
function isSessionValid(session: Session): boolean {
41+
/**
42+
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
43+
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
44+
**/
45+
async function isSessionValid(
46+
session: Session,
47+
checkLoginSettings?: boolean,
48+
): Promise<boolean> {
49+
let mfaValid = true;
50+
if (checkLoginSettings && session.factors?.user?.organizationId) {
51+
const loginSettings = await getLoginSettings(
52+
session.factors?.user?.organizationId,
53+
);
54+
if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) {
55+
const otpEmail = session.factors.otpEmail?.verifiedAt;
56+
const otpSms = session.factors.otpSms?.verifiedAt;
57+
const totp = session.factors.totp?.verifiedAt;
58+
const webAuthN = session.factors.webAuthN?.verifiedAt;
59+
60+
// must have one single check
61+
mfaValid = !!(otpEmail || otpSms || totp || webAuthN);
62+
} else {
63+
mfaValid = true;
64+
}
65+
}
66+
4167
const validPassword = session?.factors?.password?.verifiedAt;
4268
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
4369
const validIDP = session?.factors?.intent?.verifiedAt;
@@ -46,43 +72,44 @@ function isSessionValid(session: Session): boolean {
4672
? timestampDate(session.expirationDate) > new Date()
4773
: true;
4874

49-
const validFactors = !!(
50-
(validPassword || validPasskey || validIDP) &&
51-
stillValid
52-
);
75+
const validFactors = !!(validPassword || validPasskey || validIDP);
5376

54-
return stillValid && validFactors;
77+
return stillValid && validFactors && mfaValid;
5578
}
5679

57-
function findValidSession(
80+
async function findValidSession(
5881
sessions: Session[],
5982
authRequest: AuthRequest,
60-
): Session | undefined {
61-
const validSessionsWithHint = sessions
62-
.filter((s) => {
63-
if (authRequest.hintUserId) {
64-
return s.factors?.user?.id === authRequest.hintUserId;
65-
}
66-
if (authRequest.loginHint) {
67-
return s.factors?.user?.loginName === authRequest.loginHint;
68-
}
69-
return true;
70-
})
71-
.filter(isSessionValid);
83+
): Promise<Session | undefined> {
84+
const sessionsWithHint = sessions.filter((s) => {
85+
if (authRequest.hintUserId) {
86+
return s.factors?.user?.id === authRequest.hintUserId;
87+
}
88+
if (authRequest.loginHint) {
89+
return s.factors?.user?.loginName === authRequest.loginHint;
90+
}
91+
return true;
92+
});
7293

73-
if (validSessionsWithHint.length === 0) {
94+
if (sessionsWithHint.length === 0) {
7495
return undefined;
7596
}
7697

7798
// sort by change date descending
78-
validSessionsWithHint.sort((a, b) => {
99+
sessionsWithHint.sort((a, b) => {
79100
const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0;
80101
const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0;
81102
return dateB - dateA;
82103
});
83104

84-
// return most recently changed session
85-
return sessions[0];
105+
// return the first valid session according to settings
106+
for (const session of sessionsWithHint) {
107+
if (await isSessionValid(session, true)) {
108+
return session;
109+
}
110+
}
111+
112+
return undefined;
86113
}
87114

88115
export async function GET(request: NextRequest) {
@@ -103,53 +130,51 @@ export async function GET(request: NextRequest) {
103130
sessions = await loadSessions(ids);
104131
}
105132

106-
/**
107-
* TODO: before automatically redirecting to the callbackUrl, check if the session is still valid
108-
* possible scenaio:
109-
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
110-
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
111-
**/
112-
113133
if (authRequestId && sessionId) {
114134
console.log(
115135
`Login with session: ${sessionId} and authRequest: ${authRequestId}`,
116136
);
117137

118-
let selectedSession = sessions.find((s) => s.id === sessionId);
138+
const selectedSession = sessions.find((s) => s.id === sessionId);
119139

120140
if (selectedSession && selectedSession.id) {
121141
console.log(`Found session ${selectedSession.id}`);
122-
const cookie = sessionCookies.find(
123-
(cookie) => cookie.id === selectedSession?.id,
124-
);
125142

126-
if (cookie && cookie.id && cookie.token) {
127-
const session = {
128-
sessionId: cookie?.id,
129-
sessionToken: cookie?.token,
130-
};
143+
const isValid = await isSessionValid(selectedSession, true);
131144

132-
// works not with _rsc request
133-
try {
134-
const { callbackUrl } = await createCallback(
135-
create(CreateCallbackRequestSchema, {
136-
authRequestId,
137-
callbackKind: {
138-
case: "session",
139-
value: create(SessionSchema, session),
140-
},
141-
}),
142-
);
143-
if (callbackUrl) {
144-
return NextResponse.redirect(callbackUrl);
145-
} else {
146-
return NextResponse.json(
147-
{ error: "An error occurred!" },
148-
{ status: 500 },
145+
if (isValid) {
146+
const cookie = sessionCookies.find(
147+
(cookie) => cookie.id === selectedSession?.id,
148+
);
149+
150+
if (cookie && cookie.id && cookie.token) {
151+
const session = {
152+
sessionId: cookie?.id,
153+
sessionToken: cookie?.token,
154+
};
155+
156+
// works not with _rsc request
157+
try {
158+
const { callbackUrl } = await createCallback(
159+
create(CreateCallbackRequestSchema, {
160+
authRequestId,
161+
callbackKind: {
162+
case: "session",
163+
value: create(SessionSchema, session),
164+
},
165+
}),
149166
);
167+
if (callbackUrl) {
168+
return NextResponse.redirect(callbackUrl);
169+
} else {
170+
return NextResponse.json(
171+
{ error: "An error occurred!" },
172+
{ status: 500 },
173+
);
174+
}
175+
} catch (error) {
176+
return NextResponse.json({ error }, { status: 500 });
150177
}
151-
} catch (error) {
152-
return NextResponse.json({ error }, { status: 500 });
153178
}
154179
}
155180
}
@@ -314,7 +339,7 @@ export async function GET(request: NextRequest) {
314339
* This means that the user should not be prompted to enter their password again.
315340
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
316341
**/
317-
const selectedSession = findValidSession(sessions, authRequest);
342+
const selectedSession = await findValidSession(sessions, authRequest);
318343

319344
if (!selectedSession || !selectedSession.id) {
320345
return NextResponse.json(
@@ -351,7 +376,7 @@ export async function GET(request: NextRequest) {
351376
return NextResponse.redirect(callbackUrl);
352377
} else {
353378
// check for loginHint, userId hint and valid sessions
354-
let selectedSession = findValidSession(sessions, authRequest);
379+
let selectedSession = await findValidSession(sessions, authRequest);
355380

356381
if (!selectedSession || !selectedSession.id) {
357382
return gotoAccounts();

apps/login/src/components/sessions-list.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
2929
: 0;
3030
return dateB - dateA;
3131
})
32+
// TODO: add sorting to move invalid sessions to the bottom
3233
.map((session, index) => {
3334
return (
3435
<SessionItem

0 commit comments

Comments
 (0)