@@ -13,6 +13,8 @@ import {
1313 MfaPolicy ,
1414 type User ,
1515 VerificationType ,
16+ type Mfa as MfaSettings ,
17+ OrganizationRequiredMfaPolicy ,
1618} from '@logto/schemas' ;
1719import { generateStandardId } from '@logto/shared' ;
1820import { deduplicate } from '@silverhand/essentials' ;
@@ -24,6 +26,7 @@ import type Libraries from '#src/tenants/Libraries.js';
2426import type Queries from '#src/tenants/Queries.js' ;
2527import assertThat from '#src/utils/assert-that.js' ;
2628
29+ import { EnvSet } from '../../../env-set/index.js' ;
2730import { type InteractionContext } from '../types.js' ;
2831
2932import { SignInExperienceValidator } from './libraries/sign-in-experience-validator.js' ;
@@ -150,10 +153,13 @@ export class Mfa {
150153 * @throws {RequestError } with status 422 if the MFA policy is not user controlled
151154 */
152155 async skip ( ) {
153- const { policy } = await this . signInExperienceValidator . getMfaSettings ( ) ;
156+ const mfaSettings = await this . signInExperienceValidator . getMfaSettings ( ) ;
157+ const { policy } = mfaSettings ;
158+ const user = await this . interactionContext . getIdentifiedUser ( ) ;
154159
155160 assertThat (
156- policy !== MfaPolicy . Mandatory ,
161+ policy !== MfaPolicy . Mandatory &&
162+ ! ( await this . isMfaRequiredByUserOrganizations ( mfaSettings , user . id ) ) ,
157163 new RequestError ( {
158164 code : 'session.mfa.mfa_policy_not_user_controlled' ,
159165 status : 422 ,
@@ -269,23 +275,39 @@ export class Mfa {
269275 * @throws {RequestError } with status 422 if the user has not bound the backup code but enabled in the sign-in experience
270276 * @throws {RequestError } with status 422 if the user existing backup codes is empty, new backup codes is required
271277 */
278+ // eslint-disable-next-line complexity
272279 async assertUserMandatoryMfaFulfilled ( ) {
273- const { factors, policy } = await this . signInExperienceValidator . getMfaSettings ( ) ;
280+ const mfaSettings = await this . signInExperienceValidator . getMfaSettings ( ) ;
281+ const { policy, factors } = mfaSettings ;
274282
275283 // If there are no factors, then there is nothing to check
276284 if ( factors . length === 0 ) {
277285 return ;
278286 }
279287
280- const { mfaVerifications, logtoConfig } = await this . interactionContext . getIdentifiedUser ( ) ;
288+ const {
289+ mfaVerifications,
290+ logtoConfig,
291+ id : userId ,
292+ } = await this . interactionContext . getIdentifiedUser ( ) ;
281293
282- // If the policy is no prompt, then there is nothing to check
283- if ( policy === MfaPolicy . NoPrompt ) {
294+ const isMfaRequiredByUserOrganizations = await this . isMfaRequiredByUserOrganizations (
295+ mfaSettings ,
296+ userId
297+ ) ;
298+
299+ // If the policy is no prompt, and mfa is not required by the user organizations, then there is nothing to check
300+ if ( policy === MfaPolicy . NoPrompt && ! isMfaRequiredByUserOrganizations ) {
284301 return ;
285302 }
286303
287- // If the policy is not mandatory and the user has skipped MFA, then there is nothing to check
288- if ( policy !== MfaPolicy . Mandatory && ( this . #mfaSkipped ?? isMfaSkipped ( logtoConfig ) ) ) {
304+ // If the policy is not mandatory and the user has skipped MFA,
305+ // and MFA is not required by the user organizations, then there is nothing to check
306+ if (
307+ policy !== MfaPolicy . Mandatory &&
308+ ( this . #mfaSkipped ?? isMfaSkipped ( logtoConfig ) ) &&
309+ ! isMfaRequiredByUserOrganizations
310+ ) {
289311 return ;
290312 }
291313
@@ -308,7 +330,7 @@ export class Mfa {
308330 availableFactors . some ( ( factor ) => linkedFactors . includes ( factor ) ) ,
309331 new RequestError (
310332 { code : 'user.missing_mfa' , status : 422 } ,
311- policy === MfaPolicy . Mandatory
333+ policy === MfaPolicy . Mandatory || isMfaRequiredByUserOrganizations
312334 ? { availableFactors }
313335 : { availableFactors, skippable : true }
314336 )
@@ -340,4 +362,21 @@ export class Mfa {
340362
341363 assertThat ( isFactorsEnabled , new RequestError ( { code : 'session.mfa.mfa_factor_not_enabled' } ) ) ;
342364 }
365+
366+ private async isMfaRequiredByUserOrganizations ( mfaSettings : MfaSettings , userId : string ) {
367+ // TODO: Remove this check when the feature is enabled for production
368+ if ( ! EnvSet . values . isDevFeaturesEnabled ) {
369+ return false ;
370+ }
371+
372+ if ( mfaSettings . organizationRequiredMfaPolicy !== OrganizationRequiredMfaPolicy . Mandatory ) {
373+ return false ;
374+ }
375+
376+ const organizations = await this . queries . organizations . relations . users . getOrganizationsByUserId (
377+ userId
378+ ) ;
379+
380+ return organizations . some ( ( { isMfaRequired } ) => isMfaRequired ) ;
381+ }
343382}
0 commit comments