Skip to content

Commit

Permalink
check if session is valid according to loginsettings (forceMFA)
Browse files Browse the repository at this point in the history
  • Loading branch information
peintnermax committed Dec 9, 2024
1 parent ac08431 commit a4d474a
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 60 deletions.
1 change: 1 addition & 0 deletions apps/login/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,4 @@ Timebased features like the multifactor init prompt or password expiry, are not
- Lockout Settings
- Password Expiry Settings
- Login Settings: multifactor init prompt
- forceMFA on login settings is not checked for IDPs
145 changes: 85 additions & 60 deletions apps/login/src/app/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createCallback,
getActiveIdentityProviders,
getAuthRequest,
getLoginSettings,
getOrgsByDomain,
listSessions,
startIdentityProviderFlow,
Expand Down Expand Up @@ -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 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 validPasskey = session?.factors?.webAuthN?.verifiedAt;
const validIDP = session?.factors?.intent?.verifiedAt;
Expand All @@ -46,43 +72,44 @@ function isSessionValid(session: Session): boolean {
? timestampDate(session.expirationDate) > new Date()
: true;

const validFactors = !!(
(validPassword || validPasskey || validIDP) &&
stillValid
);
const validFactors = !!(validPassword || validPasskey || validIDP);

return stillValid && validFactors;
return stillValid && validFactors && mfaValid;
}

function findValidSession(
async function findValidSession(
sessions: Session[],
authRequest: AuthRequest,
): Session | undefined {
const validSessionsWithHint = 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;
})
.filter(isSessionValid);
): 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 (validSessionsWithHint.length === 0) {
if (sessionsWithHint.length === 0) {
return undefined;
}

// sort by change date descending
validSessionsWithHint.sort((a, b) => {
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 most recently changed session
return sessions[0];
// return the first valid session according to settings
for (const session of sessionsWithHint) {
if (await isSessionValid(session, true)) {
return session;
}
}

return undefined;
}

export async function GET(request: NextRequest) {
Expand All @@ -103,53 +130,51 @@ export async function GET(request: NextRequest) {
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) {
console.log(
`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) {
console.log(`Found session ${selectedSession.id}`);
const cookie = sessionCookies.find(
(cookie) => cookie.id === selectedSession?.id,
);

if (cookie && cookie.id && cookie.token) {
const session = {
sessionId: cookie?.id,
sessionToken: cookie?.token,
};
const isValid = await isSessionValid(selectedSession, true);

// works not with _rsc request
try {
const { callbackUrl } = await createCallback(
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 },
if (isValid) {
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(
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) {
return NextResponse.json({ error }, { status: 500 });
}
} catch (error) {
return NextResponse.json({ error }, { status: 500 });
}
}
}
Expand Down Expand Up @@ -314,7 +339,7 @@ export async function GET(request: NextRequest) {
* 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 = findValidSession(sessions, authRequest);
const selectedSession = await findValidSession(sessions, authRequest);

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

if (!selectedSession || !selectedSession.id) {
return gotoAccounts();
Expand Down
1 change: 1 addition & 0 deletions apps/login/src/components/sessions-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function SessionsList({ sessions, authRequestId }: Props) {
: 0;
return dateB - dateA;
})
// TODO: add sorting to move invalid sessions to the bottom
.map((session, index) => {
return (
<SessionItem
Expand Down

0 comments on commit a4d474a

Please sign in to comment.