From 26aff0ce1dd4cfd2cae8d75c507acba27d791203 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 30 Jun 2026 10:26:37 +0100 Subject: [PATCH 01/85] Add Native Auth V2 (server-driven HAL) SDK with SSPR wired end-to-end Introduce a unified, server-driven (HAL-JSON) Native Auth V2 surface that mirrors V1's internal architecture (controller / request-provider / validator / state / delegate / factory) while reusing the shared MSIDHALResource/MSIDHALLink parsing types from IdentityCore. - Public surface: MSALNativeAuthFlowError/Action/Delegate/FlowState. - Unified controller drives the full SSPR sequence (authorize-challenge -> resetpassword start -> challenge -> verify -> update -> poll -> authorize-challenge -> token). signUp/signIn V2 return notImplemented. - Network layer builds MSIDHttpRequest directly; HAL serializer + error handler funnel any HTTP status (incl. the bootstrap 401) into the validator. - Public app gains resetPasswordV2/signUpV2/signInV2; V1 signUp/signIn/ resetPassword are deprecated in favour of the V2 entry points. - Unit tests + mocks for the V2 controller and response validator. - Bump IdentityCore submodule to include the HAL serializer types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MSAL/IdentityCore | 2 +- MSAL/MSAL.xcodeproj/project.pbxproj | 224 ++++++++- MSAL/module.modulemap | 2 + .../MSALNativeAuthControllerFactory.swift | 5 + .../v2/MSALNativeAuthV2FlowController.swift | 424 ++++++++++++++++++ .../v2/MSALNativeAuthV2FlowControlling.swift | 61 +++ .../v2/MSALNativeAuthV2FlowResult.swift | 95 ++++ .../v2/MSALNativeAuthHALResponse.swift | 84 ++++ ...SALNativeAuthV2HALResponseSerializer.swift | 133 ++++++ ...MSALNativeAuthV2ResponseErrorHandler.swift | 59 +++ .../MSALNativeAuthV2ResponseValidator.swift | 158 +++++++ .../MSALNativeAuthV2ValidatedResponses.swift | 90 ++++ .../network/v2/MSALNativeAuthV2Endpoint.swift | 39 ++ .../v2/MSALNativeAuthV2HrefURLResolver.swift | 114 +++++ .../v2/MSALNativeAuthV2RequestProvider.swift | 199 ++++++++ ...SALNativeAuthPublicClientApplication.swift | 55 ++- .../v2/MSALNativeAuthAction.swift | 67 +++ .../v2/MSALNativeAuthFlowDelegate.swift | 72 +++ .../v2/MSALNativeAuthFlowError.swift | 98 ++++ .../v2/MSALNativeAuthFlowState.swift | 105 +++++ .../MSALNativeAuthV2FlowControllerTests.swift | 265 +++++++++++ .../mock/MSALNativeAuthFactoriesMocks.swift | 14 +- .../MSALNativeAuthV2FlowControllerMock.swift | 92 ++++ .../MSALNativeAuthV2RequestProviderMock.swift | 103 +++++ ...SALNativeAuthV2ResponseValidatorMock.swift | 58 +++ ...ALNativeAuthV2ResponseValidatorTests.swift | 176 ++++++++ 26 files changed, 2774 insertions(+), 20 deletions(-) create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift create mode 100644 MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift create mode 100644 MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift create mode 100644 MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift create mode 100644 MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift create mode 100644 MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift diff --git a/MSAL/IdentityCore b/MSAL/IdentityCore index 00ab949b45..0d6ae66c7f 160000 --- a/MSAL/IdentityCore +++ b/MSAL/IdentityCore @@ -1 +1 @@ -Subproject commit 00ab949b45b6423cf1cc054fb90cb08f6db2d694 +Subproject commit 0d6ae66c7f2ba594a2d3c824545c8cb9dc8860bf diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 45b00ed1dd..47fde98430 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; 04A6B5AE226936F30035C7C2 /* MSALFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F5BC91E59359900912CB8 /* MSALFramework.m */; }; 04A6B5AF226936F40035C7C2 /* MSALFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F5BC91E59359900912CB8 /* MSALFramework.m */; }; 04A6B5B0226936FE0035C7C2 /* MSIDVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = B2C17B091FC8DB2E0070A514 /* MSIDVersion.m */; }; @@ -87,8 +88,11 @@ 0D96DB3C27850F0F00DEAF87 /* MSALWipeCacheForAllAccountsConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D96DB2E27850E1300DEAF87 /* MSALWipeCacheForAllAccountsConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0D96DB3D27850F1100DEAF87 /* MSALWipeCacheForAllAccountsConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D96DB2E27850E1300DEAF87 /* MSALWipeCacheForAllAccountsConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0D96DB3E27850F1200DEAF87 /* MSALWipeCacheForAllAccountsConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D96DB2E27850E1300DEAF87 /* MSALWipeCacheForAllAccountsConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */; }; 12E2160B2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 12E2160C2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; + 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; + 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; 1E1A2E042256D12F001009ED /* MSALTestAppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64B01E5AAC5C0086D120 /* MSALTestAppSettings.m */; }; @@ -388,14 +392,22 @@ 28FDC4A62A38C00900E38BE1 /* SignInAfterSignUpDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDC4A52A38C00900E38BE1 /* SignInAfterSignUpDelegate.swift */; }; 28FDC4A92A38C0D100E38BE1 /* SignInAfterSignUpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDC4A82A38C0D000E38BE1 /* SignInAfterSignUpError.swift */; }; 28FDC4AE2A38D81100E38BE1 /* MSALNativeAuthSignInControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDC4AB2A38D7D200E38BE1 /* MSALNativeAuthSignInControllerMock.swift */; }; + 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; + 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; + 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; + 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */; }; + 33A0542A5B652892314FD6C8 /* MSALNativeAuthV2FlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */; }; 38880DF423280C5900688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 38880DF523280C5A00688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; + 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; 583BFD0F24DC8E670035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; 583BFD1024DC8EE80035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; 583BFD1624DDF9B10035B901 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */; }; 58B81F7124AC5D7200E8799E /* MSALTestCacheTokenResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */; }; 58B81F7224AC5D7300E8799E /* MSALTestCacheTokenResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */; }; 58BBA11E25C1406F007B3EF6 /* MSAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; }; + 5E471E84AA33CFA840BBA964 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; 6077D4A022498BFF001798A2 /* MSALTenantProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 6077D49F22498BFF001798A2 /* MSALTenantProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6077D4A122498BFF001798A2 /* MSALTenantProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 6077D49F22498BFF001798A2 /* MSALTenantProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6077D4A922498D87001798A2 /* MSALTenantProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 6077D4A822498D87001798A2 /* MSALTenantProfile.m */; }; @@ -403,6 +415,9 @@ 609AF9332256BD0C00E2978D /* MSALAccountsProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609AF9322256BD0C00E2978D /* MSALAccountsProviderTests.m */; }; 6525115A29CD84A000D3B876 /* MSALPublicClientApplicationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D673F07C1E4AAB0D0018BA91 /* MSALPublicClientApplicationTests.m */; }; 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; + 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; + 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; 7207E63A2FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -412,6 +427,7 @@ 7207E6402FA97BBD008F6803 /* MSALDeviceTokenParametersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7207E63E2FA97BBC008F6803 /* MSALDeviceTokenParametersTests.m */; }; 7207E6432FA97BE4008F6803 /* MSALDeviceTokenResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7207E6422FA97BE3008F6803 /* MSALDeviceTokenResultTests.m */; }; 7207E6442FA97BE4008F6803 /* MSALDeviceTokenResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7207E6422FA97BE3008F6803 /* MSALDeviceTokenResultTests.m */; }; + 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; 7233F07F2F885A4A009C9602 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7233F0812F885A4A009C9602 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7233F0822F885A4A009C9602 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -427,6 +443,10 @@ 7248CF9C2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 7248CF9D2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 7248CF9E2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; + 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */; }; + 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; + 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; + 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; 886F515829CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; 886F515929CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; 886F515A29CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; }; @@ -441,6 +461,8 @@ 8D35C8F12A97BD2300BEC29A /* MSALNativeAuthRequiredAttributeOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D35C8F02A97BD2300BEC29A /* MSALNativeAuthRequiredAttributeOptions.swift */; }; 8D61F9A12A66AC9D00468E18 /* MSALNativeAuthRequestableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61F9A02A66AC9D00468E18 /* MSALNativeAuthRequestableTests.swift */; }; 8DDF473F2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDF473E2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift */; }; + 8E0486CA55F25C1987E4067A /* MSALNativeAuthV2FlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */; }; + 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthV2FlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */; }; 91AA24592BDF643A005037EA /* MSAL_Test_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA24582BDF643A005037EA /* MSAL_Test_App.swift */; }; 91AA245B2BDF643A005037EA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA245A2BDF643A005037EA /* ContentView.swift */; }; 91AA245D2BDF6440005037EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 91AA245C2BDF6440005037EA /* Assets.xcassets */; }; @@ -453,6 +475,7 @@ 960751BB2183E82C00F2BF2F /* MSALAccountIdTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 960751BA2183E82C00F2BF2F /* MSALAccountIdTests.m */; }; 960751BC2183E82C00F2BF2F /* MSALAccountIdTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 960751BA2183E82C00F2BF2F /* MSALAccountIdTests.m */; }; 96090D9020E58DE600E42B37 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DEC20E1574F00200E6F /* WebKit.framework */; }; + 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */; }; 962302591E7215170022A778 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 962302581E7215170022A778 /* Launch Screen.storyboard */; }; 9626D14D225828780019417B /* MSALGlobalConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 9626D14A225828780019417B /* MSALGlobalConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9626D14E225828780019417B /* MSALGlobalConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 9626D14B225828780019417B /* MSALGlobalConfig.m */; }; @@ -470,6 +493,7 @@ 963377C1211E14C600943EE0 /* MSALWebviewType.m in Sources */ = {isa = PBXBuildFile; fileRef = 963377BE211E14C600943EE0 /* MSALWebviewType.m */; }; 963377C2211E14C600943EE0 /* MSALWebviewType.m in Sources */ = {isa = PBXBuildFile; fileRef = 963377BE211E14C600943EE0 /* MSALWebviewType.m */; }; 963C89AB214BA1760051AFEE /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */; }; 9682A630218290FE00E37E63 /* MSALDefinitions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9682A62A218290F700E37E63 /* MSALDefinitions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96902DF320E1577500200E6F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DEC20E1574F00200E6F /* WebKit.framework */; }; 96902DF420E1578700200E6F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DEC20E1574F00200E6F /* WebKit.framework */; }; @@ -552,6 +576,7 @@ 9D02FCB728EF33FE003F791C /* MSALWPJMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 9DA6473528EC2FF10014F44F /* MSALWPJMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9D292B1028F05696007FE93C /* MSALWPJMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D292B0F28F05696007FE93C /* MSALWPJMetaData.m */; }; 9D292B1128F05696007FE93C /* MSALWPJMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D292B0F28F05696007FE93C /* MSALWPJMetaData.m */; }; + 9D57981C31A9157AF52A29B9 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */; }; A0274CBE24B432B100BD198D /* MSALAuthSchemeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */; }; A0274CBF24B432B100BD198D /* MSALAuthSchemeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */; }; A0274CD824B54A4E00BD198D /* MSALDevicePopManagerUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */; }; @@ -562,6 +587,10 @@ A0274CDE24B54C8900BD198D /* MSALDevicePopManagerUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */; }; A09AAFC324C00B3600C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A09AAFC424C00B3700C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A370E8BCE6A05E05ECC63027 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */; }; + A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; + A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; + A939579E9B632F2EFA0447E6 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */; }; B203459521AF77FB00B221AA /* MSALRedirectUri.h in Headers */ = {isa = PBXBuildFile; fileRef = B203459221AF77FB00B221AA /* MSALRedirectUri.h */; settings = {ATTRIBUTES = (Public, ); }; }; B203459621AF77FB00B221AA /* MSALRedirectUri.m in Sources */ = {isa = PBXBuildFile; fileRef = B203459321AF77FB00B221AA /* MSALRedirectUri.m */; }; B203459721AF77FC00B221AA /* MSALRedirectUri.m in Sources */ = {isa = PBXBuildFile; fileRef = B203459321AF77FB00B221AA /* MSALRedirectUri.m */; }; @@ -960,6 +989,12 @@ B2FBB3DA28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FBB3DB28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FE601B20E5BB5800502BA6 /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; + C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; + CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; D61A64941E5AA7D60086D120 /* MSALTestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64801E5AA7C60086D120 /* MSALTestAppDelegate.m */; }; D61A64951E5AA7D60086D120 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64811E5AA7C60086D120 /* main.m */; }; D61A64A91E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A649D1E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m */; }; @@ -1543,6 +1578,7 @@ DEFE87722CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; DEFE87732CA6BC91009D11DC /* CredentialsDelegateSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876E2CA6BC91009D11DC /* CredentialsDelegateSpies.swift */; }; DEFE87742CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; + E04298BA8ED8FBE431F561A2 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */; }; E2025CC92B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025CC82B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift */; }; E2025D202B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025D1F2B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift */; }; E205D62E29B783FF003887BC /* MSALNativeAuthInternalConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E205D62D29B783FF003887BC /* MSALNativeAuthInternalConfiguration.swift */; }; @@ -1671,6 +1707,10 @@ E2F626B32A781CE300C4A303 /* SignInDelegatesSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F626B22A781CE300C4A303 /* SignInDelegatesSpies.swift */; }; E2F890052B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */; }; E2F8900E2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */; }; + E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; + E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; + E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2027,6 +2067,7 @@ 0D96DB2E27850E1300DEAF87 /* MSALWipeCacheForAllAccountsConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALWipeCacheForAllAccountsConfig.h; sourceTree = ""; }; 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWipeCacheForAllAccountsConfig.m; sourceTree = ""; }; 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorityURLFormat.swift; sourceTree = ""; }; + 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ValidatedResponses.swift; sourceTree = ""; }; 1E04571F24BD5A7D00444756 /* MSALCacheItemDetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCacheItemDetailViewController.h; sourceTree = ""; }; 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCacheItemDetailViewController.m; sourceTree = ""; }; 1E1A2E052256D194001009ED /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -2208,6 +2249,9 @@ 28FDC4A52A38C00900E38BE1 /* SignInAfterSignUpDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInAfterSignUpDelegate.swift; sourceTree = ""; }; 28FDC4A82A38C0D000E38BE1 /* SignInAfterSignUpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInAfterSignUpError.swift; sourceTree = ""; }; 28FDC4AB2A38D7D200E38BE1 /* MSALNativeAuthSignInControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInControllerMock.swift; sourceTree = ""; }; + 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseErrorHandler.swift; sourceTree = ""; }; + 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; + 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 58B81F6524AC59A000E8799E /* MSALTestCacheTokenResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTestCacheTokenResponse.h; sourceTree = ""; }; 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTestCacheTokenResponse.m; sourceTree = ""; }; @@ -2216,6 +2260,9 @@ 609AF9322256BD0C00E2978D /* MSALAccountsProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAccountsProviderTests.m; sourceTree = ""; }; 609AF958225B348900E2978D /* MSALTenantProfile+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTenantProfile+Internal.h"; sourceTree = ""; }; 60DEF15A1E67756800966664 /* MSAL Test App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = "MSAL Test App.entitlements"; path = "../../../../MSAL Test App.entitlements"; sourceTree = ""; }; + 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowState.swift; sourceTree = ""; }; + 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControllerMock.swift; sourceTree = ""; }; + 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidator.swift; sourceTree = ""; }; 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALDeviceTokenResult+Internal.h"; sourceTree = ""; }; 7207E63E2FA97BBC008F6803 /* MSALDeviceTokenParametersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenParametersTests.m; sourceTree = ""; }; 7207E6422FA97BE3008F6803 /* MSALDeviceTokenResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenResultTests.m; sourceTree = ""; }; @@ -2223,6 +2270,9 @@ 7233F0882F885D05009C9602 /* MSALDeviceTokenParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenParameters.m; sourceTree = ""; }; 7248CF8E2F9AF2E90038E238 /* MSALDeviceTokenResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDeviceTokenResult.h; sourceTree = ""; }; 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenResult.m; sourceTree = ""; }; + 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControllerTests.swift; sourceTree = ""; }; + 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowResult.swift; sourceTree = ""; }; + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowError.swift; sourceTree = ""; }; 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCIAMAuthority.h; sourceTree = ""; }; 886F516329CCA58900F09471 /* MSALCIAMAuthority.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthority.m; sourceTree = ""; }; 88A25ED229E7185B00066311 /* MSALCIAMAuthorityTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthorityTests.m; sourceTree = ""; }; @@ -2265,6 +2315,7 @@ 963377BD211E14C600943EE0 /* MSALWebviewType_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALWebviewType_Internal.h; sourceTree = ""; }; 963377BE211E14C600943EE0 /* MSALWebviewType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWebviewType.m; sourceTree = ""; }; 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; + 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALResponse.swift; sourceTree = ""; }; 9648AF54225D826500F66801 /* MSALTelemetryConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTelemetryConfig+Internal.h"; sourceTree = ""; }; 9648AF5B225DD6A900F66801 /* MSALGlobalConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALGlobalConfig+Internal.h"; sourceTree = ""; }; 9682A62A218290F700E37E63 /* MSALDefinitions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDefinitions.h; sourceTree = ""; }; @@ -2318,9 +2369,12 @@ 9BE7E3D42A1CF51500CC3A62 /* MSALNativeAuthResetPasswordValidatedResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordValidatedResponses.swift; sourceTree = ""; }; 9D292B0F28F05696007FE93C /* MSALWPJMetaData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSALWPJMetaData.m; sourceTree = ""; }; 9DA6473528EC2FF10014F44F /* MSALWPJMetaData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALWPJMetaData.h; sourceTree = ""; }; + 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorMock.swift; sourceTree = ""; }; A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAuthSchemeTests.m; sourceTree = ""; }; A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDevicePopManagerUtil.m; sourceTree = ""; }; A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; + AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAction.swift; sourceTree = ""; }; + B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProvider.swift; sourceTree = ""; }; B203459221AF77FB00B221AA /* MSALRedirectUri.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALRedirectUri.h; sourceTree = ""; }; B203459321AF77FB00B221AA /* MSALRedirectUri.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALRedirectUri.m; sourceTree = ""; }; B203459C21AFA1FB00B221AA /* MSALRedirectUri+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALRedirectUri+Internal.h"; sourceTree = ""; }; @@ -2476,6 +2530,11 @@ B2F4572F211C0B5C00818910 /* MSALBaseAADUITest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALBaseAADUITest.h; sourceTree = ""; }; B2F45744211E41C100818910 /* MSALB2CInteractiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALB2CInteractiveTests.m; sourceTree = ""; }; B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALWPJMetaData+Internal.h"; sourceTree = ""; }; + B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControlling.swift; sourceTree = ""; }; + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowDelegate.swift; sourceTree = ""; }; + C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; + C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowController.swift; sourceTree = ""; }; + C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorTests.swift; sourceTree = ""; }; D61A63F11E5979200086D120 /* MSALResult+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALResult+Internal.h"; sourceTree = ""; }; D61A64331E5A29580086D120 /* MSAL Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MSAL Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D61A64661E5AA6B40086D120 /* msal__test_app__ios.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = msal__test_app__ios.xcconfig; sourceTree = ""; }; @@ -2858,6 +2917,7 @@ E2F626B22A781CE300C4A303 /* SignInDelegatesSpies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInDelegatesSpies.swift; sourceTree = ""; }; E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUnknownCaseProtocol.swift; sourceTree = ""; }; E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUnknownCaseProtocolTests.swift; sourceTree = ""; }; + F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolver.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3049,6 +3109,38 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 03D8CB5A8588FA78B92F2A2D /* v2 */ = { + isa = PBXGroup; + children = ( + 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */, + F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */, + B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 1718011BA0E3C2154663D13F /* v2 */ = { + isa = PBXGroup; + children = ( + 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */, + B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */, + C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 1A153D161101EAF09A32E906 /* validator */ = { + isa = PBXGroup; + children = ( + 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */, + 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */, + ); + name = validator; + path = validator; + sourceTree = ""; + }; 2811CDCF296F16DE007BA21B /* controllers */ = { isa = PBXGroup; children = ( @@ -3062,6 +3154,7 @@ E2C1D286299BA15D00B26449 /* MSALNativeAuthBaseController.swift */, DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */, E2EFAD152A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift */, + 1718011BA0E3C2154663D13F /* v2 */, ); path = controllers; sourceTree = ""; @@ -3154,6 +3247,7 @@ DEFE87682CA6BC3A009D11DC /* MSALNativeAuthSilentTokenProviderFactoryMock.swift */, DEFE87692CA6BC3A009D11DC /* MSALNativeAuthSilentTokenProviderMock.swift */, 9B61C91D2A27E5E200CE9E3A /* reset_password */, + 9279009AB4669F3B2A4D66FD /* v2 */, ); path = mock; sourceTree = ""; @@ -3203,6 +3297,7 @@ E2DDF1B22B6A9E1D00E9FAB7 /* MSALNativeAuthCustomErrorSerializerTests.swift */, E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */, 289C1D8E2DE8C669009EEBEA /* MSALNativeAuthInternalConfigurationTest.swift */, + 4E0AA7DA6DFC949218EEA54E /* v2 */, ); path = network; sourceTree = ""; @@ -3324,6 +3419,7 @@ 28DCD09529D7170F00C4601E /* state */, 28DCD09029D7165700C4601E /* delegate */, 28F8D29C2D8C6B1C005084FA /* result */, + F58967D39C050697CB65B47C /* v2 */, ); path = state_machine; sourceTree = ""; @@ -3437,6 +3533,27 @@ path = result; sourceTree = ""; }; + 4583FB32E02B0075F72EB043 /* v2 */ = { + isa = PBXGroup; + children = ( + 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */, + C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */, + 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */, + 1A153D161101EAF09A32E906 /* validator */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 4E0AA7DA6DFC949218EEA54E /* v2 */ = { + isa = PBXGroup; + children = ( + C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; 91AA245E2BDF6441005037EA /* Preview Content */ = { isa = PBXGroup; children = ( @@ -3469,6 +3586,17 @@ path = resources; sourceTree = ""; }; + 9279009AB4669F3B2A4D66FD /* v2 */ = { + isa = PBXGroup; + children = ( + 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */, + 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */, + 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; 94E876C91E492D2800FB96ED /* instance */ = { isa = PBXGroup; children = ( @@ -4430,6 +4558,7 @@ E243F69229D1973900DAC60F /* sign_up */, DEE34F51D170B71C00BC302A /* reset_password */, DE0FECAA2993AD3700B139A8 /* MSALNativeAuthResendCodeRequestResponse.swift */, + 4583FB32E02B0075F72EB043 /* v2 */, ); path = responses; sourceTree = ""; @@ -5180,6 +5309,7 @@ DEDB29A229DDA992008DA85B /* errors */, E235613329C9D528000E01CA /* MSALNativeAuthInternalChallengeType.swift */, 289C1D8B2DE899B7009EEBEA /* MSALNativeAuthInternalCapability.swift */, + 03D8CB5A8588FA78B92F2A2D /* v2 */, ); path = network; sourceTree = ""; @@ -5279,6 +5409,7 @@ 9B4EE9CD2A1686A900F243C1 /* MSALNativeAuthResetPasswordControllerTests.swift */, DE14096C2A38DF40008E6F1E /* MSALNativeAuthCredentialsControllerTests.swift */, 28A600A92C78E09F00455666 /* MSALNativeAuthMFAControllerTests.swift */, + E6E09369971B71B5529C8CFE /* v2 */, ); path = controllers; sourceTree = ""; @@ -5291,6 +5422,27 @@ path = factories; sourceTree = ""; }; + E6E09369971B71B5529C8CFE /* v2 */ = { + isa = PBXGroup; + children = ( + 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + F58967D39C050697CB65B47C /* v2 */ = { + isa = PBXGroup; + children = ( + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, + AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */, + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, + 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -5812,8 +5964,6 @@ 91AA24802BDF6DC1005037EA /* PBXTargetDependency */, ); name = "MSAL Test App (visionOS)"; - packageProductDependencies = ( - ); productName = "MSAL Test App (visionOS)"; productReference = 91AA24522BDF6439005037EA /* MSAL Test App.app */; productType = "com.apple.product-type.application"; @@ -7161,6 +7311,21 @@ E2EFAD162A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift in Sources */, 285F58542C5BA33B00F4EFA4 /* MSALNativeAuthSignInIntrospectRequestParameters.swift in Sources */, 28DE70D629FAC16700EB75AA /* MSALNativeAuthSignInResponseValidator.swift in Sources */, + BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */, + C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */, + BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */, + 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowState.swift in Sources */, + 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthV2FlowResult.swift in Sources */, + 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */, + 8E0486CA55F25C1987E4067A /* MSALNativeAuthV2FlowController.swift in Sources */, + CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */, + 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, + 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, + B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, + 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseValidator.swift in Sources */, + 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */, + 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, + 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7449,6 +7614,21 @@ DE8DC4612C66219600534E8F /* SignUpResults.swift in Sources */, DE8DC4972C6621A600534E8F /* SignInAfterResetPasswordDelegate.swift in Sources */, DE8DC4512C66218900534E8F /* MSALNativeAuthInternalError.swift in Sources */, + 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */, + E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */, + E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */, + 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowState.swift in Sources */, + 33A0542A5B652892314FD6C8 /* MSALNativeAuthV2FlowResult.swift in Sources */, + E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */, + 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */, + 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */, + 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, + 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, + 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, + 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */, + 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */, + 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, + 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7639,6 +7819,11 @@ 9BD2765B2A0E7E7D00FBD033 /* ResetPasswordCodeSentStateTests.swift in Sources */, E2F626B32A781CE300C4A303 /* SignInDelegatesSpies.swift in Sources */, E22427EA2B065EAE0006C55E /* SignUpVerifyCodeDelegateDispatcherTests.swift in Sources */, + A370E8BCE6A05E05ECC63027 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */, + 5E471E84AA33CFA840BBA964 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, + D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, + E04298BA8ED8FBE431F561A2 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */, + A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7817,6 +8002,11 @@ 28FB1BB62E0AF1F90065B784 /* MSALNativeAuthPublicClientApplicationConfigObjCTest.m in Sources */, 289E44BA2C9D843F00F6B9D7 /* MFARequestChallengeErrorTests.swift in Sources */, DE8DC56C2C66221C00534E8F /* MSALNativeLoggingTests.swift in Sources */, + A939579E9B632F2EFA0447E6 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */, + 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, + F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, + 9D57981C31A9157AF52A29B9 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */, + A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8323,7 +8513,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.4; @@ -8468,7 +8658,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -8486,7 +8676,7 @@ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/unit-test-host.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/unit-test-host"; USER_HEADER_SEARCH_PATHS = ( "$(inherited)", - $IDCORE_PATH/src, + "$IDCORE_PATH/src", ); }; name = Debug; @@ -8541,7 +8731,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; @@ -8558,7 +8748,7 @@ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/unit-test-host.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/unit-test-host"; USER_HEADER_SEARCH_PATHS = ( "$(inherited)", - $IDCORE_PATH/src, + "$IDCORE_PATH/src", ); VALIDATE_PRODUCT = YES; }; @@ -9235,7 +9425,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = 0; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9264,7 +9454,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9291,7 +9481,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; GCC_OPTIMIZATION_LEVEL = 0; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -9313,7 +9503,7 @@ CODE_SIGN_IDENTITY = "-"; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", @@ -9337,7 +9527,7 @@ DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = 0; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9369,7 +9559,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -9398,7 +9588,7 @@ CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; GCC_OPTIMIZATION_LEVEL = 0; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -9419,7 +9609,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -9541,7 +9731,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; @@ -9612,7 +9802,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; - HEADER_SEARCH_PATHS = $SRCROOT; + HEADER_SEARCH_PATHS = "$SRCROOT"; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; diff --git a/MSAL/module.modulemap b/MSAL/module.modulemap index dd2fe8668c..10ee87773b 100644 --- a/MSAL/module.modulemap +++ b/MSAL/module.modulemap @@ -69,6 +69,8 @@ module MSAL_Private { header "IdentityCore/IdentityCore/src/validation/MSIDAuthority+Internal.h" header "IdentityCore/IdentityCore/src/network/response_serializer/MSIDResponseSerialization.h" header "IdentityCore/IdentityCore/src/network/response_serializer/MSIDAADTokenResponseSerializer.h" + header "IdentityCore/IdentityCore/src/network/response_serializer/hal/MSIDHALResource.h" + header "IdentityCore/IdentityCore/src/network/response_serializer/hal/MSIDHALLink.h" header "IdentityCore/IdentityCore/src/requests/sdk/MSIDTokenResponseValidator.h" header "IdentityCore/IdentityCore/src/MSIDError.h" header "IdentityCore/IdentityCore/src/MSIDTelemetryStringSerializable.h" diff --git a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift index 30052d83d9..44cd8e1bc1 100644 --- a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift +++ b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift @@ -28,6 +28,7 @@ protocol MSALNativeAuthControllerBuildable { func makeJITController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthJITControlling func makeResetPasswordController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthResetPasswordControlling func makeCredentialsController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthCredentialsControlling + func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthV2FlowControlling } final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { @@ -56,4 +57,8 @@ final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { func makeCredentialsController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthCredentialsControlling { return MSALNativeAuthCredentialsController(config: config, cacheAccessor: cacheAccessor) } + + func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthV2FlowControlling { + return MSALNativeAuthV2FlowController(config: config, cacheAccessor: cacheAccessor) + } } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift new file mode 100644 index 0000000000..437734ee14 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -0,0 +1,424 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +/// Unified controller backing the Native Auth V2 (server-driven, HAL) flows. +/// +/// Mirrors the V1 controller structure (telemetry → request → validate → handle) but drives +/// the server-driven SSPR state machine end-to-end: bootstrap → reset-password start → +/// challenge → verify → update → poll → authorize-challenge → token. Sign up / sign in V2 are +/// defined for the unified contract but return `notImplemented` until their server APIs ship. +final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNativeAuthV2FlowControlling { + + private let config: MSALNativeAuthInternalConfiguration + private let requestProvider: MSALNativeAuthV2RequestProviding + private let responseValidator: MSALNativeAuthV2ResponseValidating + private let cacheAccessor: MSALNativeAuthCacheInterface + + private let maxPollAttempts = 5 + private let pollIntervalNanoseconds: UInt64 = 1_500_000_000 // 1.5s + + init( + config: MSALNativeAuthInternalConfiguration, + requestProvider: MSALNativeAuthV2RequestProviding, + responseValidator: MSALNativeAuthV2ResponseValidating, + cacheAccessor: MSALNativeAuthCacheInterface + ) { + self.config = config + self.requestProvider = requestProvider + self.responseValidator = responseValidator + self.cacheAccessor = cacheAccessor + super.init(clientId: config.clientId) + } + + convenience init(config: MSALNativeAuthInternalConfiguration, cacheAccessor: MSALNativeAuthCacheInterface) { + self.init( + config: config, + requestProvider: MSALNativeAuthV2RequestProvider(config: config), + responseValidator: MSALNativeAuthV2ResponseValidator(), + cacheAccessor: cacheAccessor + ) + } + + // MARK: - Entry points + + func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) + + // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token). + let bootstrap = await performAuthorizeChallengeStart(context: context) + guard case .continuationToken(let bootstrapToken, _) = bootstrap else { + return failure(bootstrap, event: event, context: context) + } + + // Step 2 — reset-password start. + let startResult = await performInteraction(context: context) { + try self.requestProvider.resetPasswordStart(username: parameters.username, continuationToken: bootstrapToken, context: context) + } + + guard case .challengeRequired(let token2, let challengeHref, let hint) = startResult else { + return interactionFailure(startResult, event: event, context: context, newState: nil) + } + + // Step 3 — auto-trigger the challenge (send EOTP). + guard let challengeHref = challengeHref else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link")), event: event, context: context) + } + + let challengeResult = await performInteraction(context: context) { + try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) + } + + return handleCodeRequired(challengeResult, username: parameters.username, fallbackHint: hint, event: event, context: context) + } + + func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { + return notImplemented(apiId: .telemetryApiIdSignUp, correlationId: parameters.correlationId, flow: "Sign up V2") + } + + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { + return notImplemented(apiId: .telemetryApiIdSignInWithCodeStart, correlationId: parameters.correlationId, flow: "Sign in V2") + } + + // MARK: - Continuation + + func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) + let continuation = state.continuation + + guard let verifyHref = continuation.link("verify")?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), event: event, context: context) + } + + let result = await performInteraction(context: context) { + try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) + } + + switch result { + case .updateRequired(let token, let updateHref): + let newState = makeState(.resetPassword, continuationToken: token, links: ["update": updateHref], username: continuation.username) + stopTelemetryEvent(event, context: context) + return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) + case .error(let error): + // Recoverable: allow the app to retry with the same code-required state. + return interactionFailure(result, event: event, context: context, newState: error.isInvalidCode ? state : nil) + default: + return interactionFailure(result, event: event, context: context, newState: nil) + } + } + + func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return notImplemented(apiId: .telemetryApiIdSignInSubmitPassword, correlationId: nil, flow: "Submit password V2") + } + + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmit, context: context) + let continuation = state.continuation + + guard let updateHref = continuation.link("update")?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing update link")), event: event, context: context) + } + + // Step 5 — update password. + let updateResult = await performInteraction(context: context) { + try self.requestProvider.updatePassword(href: updateHref, newPassword: password, continuationToken: continuation.continuationToken, context: context) + } + + guard case .pollInProgress(var pollToken, let pollHref) = updateResult else { + return interactionFailure(updateResult, event: event, context: context, newState: nil) + } + + guard let pollHref = pollHref else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing poll link")), event: event, context: context) + } + + // Step 6 — poll until the operation completes. + var completionToken: String? + for attempt in 0.. 0 { + try? await Task.sleep(nanoseconds: pollIntervalNanoseconds) + } + + let pollResult = await performInteraction(context: context) { + try self.requestProvider.poll(href: pollHref, continuationToken: pollToken, context: context) + } + + switch pollResult { + case .readyToComplete(let token): + completionToken = token + case .pollInProgress(let token, _): + pollToken = token + continue + case .error: + return interactionFailure(pollResult, event: event, context: context, newState: nil) + default: + return interactionFailure(pollResult, event: event, context: context, newState: nil) + } + + if completionToken != nil { + break + } + } + + guard let completionToken = completionToken else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Password reset did not complete in time")), event: event, context: context) + } + + // Step 7 — completion authorize-challenge → authorization code. + let codeResult = await performAuthorizeChallengeContinue(continuationToken: completionToken, context: context) + guard case .authorizationCode(let code) = codeResult else { + return failure(codeResult, event: event, context: context) + } + + // Step 8 — token exchange. + let tokenRequestResult: Result = await send { + try self.requestProvider.token(code: code, context: context) + } + let tokenResult = responseValidator.validateToken(tokenRequestResult) + + switch tokenResult { + case .success: + guard let accountResult = makeUserAccountResult(username: continuation.username, context: context) else { + let error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to construct account result") + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + stopTelemetryEvent(event, context: context) + return response(.completed(accountResult), context: context) + case .error(let error): + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + } + + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return notImplemented(apiId: .telemetryApiIdSignUpSubmitAttributes, correlationId: nil, flow: "Submit attributes V2") + } + + func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + state: MSALNativeAuthFlowState + ) async -> MSALNativeAuthV2FlowControllerResponse { + return notImplemented(apiId: .telemetryApiIdMFAGetAuthMethods, correlationId: nil, flow: "Select auth method V2") + } + + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return notImplemented(apiId: .telemetryApiIdMFASubmitChallenge, correlationId: nil, flow: "Submit challenge V2") + } + + func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordResendCode, context: context) + let continuation = state.continuation + + guard let resendHref = continuation.link("resend")?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing resend link")), event: event, context: context) + } + + let result = await performInteraction(context: context) { + try self.requestProvider.challenge(href: resendHref, continuationToken: continuation.continuationToken, context: context) + } + + return handleCodeRequired(result, username: continuation.username, fallbackHint: continuation.sentToHint, event: event, context: context) + } + + // MARK: - Shared step helpers + + private func performAuthorizeChallengeStart( + context: MSALNativeAuthRequestContext + ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + let result: Result = await send { + try self.requestProvider.authorizeChallengeStart(context: context) + } + return responseValidator.validateAuthorizeChallenge(result) + } + + private func performAuthorizeChallengeContinue( + continuationToken: String, + context: MSALNativeAuthRequestContext + ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + let result: Result = await send { + try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, context: context) + } + return responseValidator.validateAuthorizeChallenge(result) + } + + private func performInteraction( + context: MSALNativeAuthRequestContext, + requestBuilder: @escaping () throws -> MSIDHttpRequest + ) async -> MSALNativeAuthV2InteractionValidatedResponse { + let result: Result = await send(requestBuilder) + return responseValidator.validateInteraction(result) + } + + private func send( + _ requestBuilder: @escaping () throws -> MSIDHttpRequest + ) async -> Result { + let context = MSALNativeAuthRequestContext(correlationId: nil) + do { + let request = try requestBuilder() + let typedContext = (request.context as? MSALNativeAuthRequestContext) ?? context + return await performRequest(request, context: typedContext) + } catch { + return .failure(error) + } + } + + // MARK: - Result mapping + + private func handleCodeRequired( + _ result: MSALNativeAuthV2InteractionValidatedResponse, + username: String?, + fallbackHint: String?, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext + ) -> MSALNativeAuthV2FlowControllerResponse { + switch result { + case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): + let newState = makeState( + .resetPassword, + continuationToken: token, + links: ["verify": verifyHref, "resend": resendHref], + username: username, + sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, + codeLength: codeLength + ) + let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo + stopTelemetryEvent(event, context: context) + return response( + .actionRequired( + action: .codeRequired(sentTo: displaySentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + newState: newState + ), + context: context + ) + default: + return interactionFailure(result, event: event, context: context, newState: nil) + } + } + + private func makeState( + _ flowType: MSALNativeAuthV2FlowType, + continuationToken: String, + links: [String: String?], + username: String?, + sentToHint: String? = nil, + codeLength: Int? = nil + ) -> MSALNativeAuthFlowState { + let resolver = MSALNativeAuthV2HrefURLResolver(config: config) + var resolvedLinks: [String: URL] = [:] + for (relation, href) in links { + if let href = href, let url = try? resolver.url(forHref: href) { + resolvedLinks[relation] = url + } + } + let continuation = MSALNativeAuthV2ContinuationState( + flowType: flowType, + continuationToken: continuationToken, + links: resolvedLinks, + username: username, + sentToHint: sentToHint, + codeLength: codeLength + ) + return MSALNativeAuthFlowState(continuation: continuation, controller: self) + } + + private func makeUserAccountResult(username: String?, context: MSALNativeAuthRequestContext) -> MSALNativeAuthUserAccountResult? { + let environment = config.authority.url.host ?? "login.microsoftonline.com" + let homeAccountId = MSALAccountId(accountIdentifier: "", objectId: "", tenantId: "") + guard let account = MSALAccount( + username: username ?? "", + homeAccountId: homeAccountId, + environment: environment, + tenantProfiles: [] + ) else { + return nil + } + return MSALNativeAuthUserAccountResult( + account: account, + rawIdToken: nil, + configuration: config, + cacheAccessor: cacheAccessor + ) + } + + // MARK: - Response construction + + private func response( + _ result: MSALNativeAuthV2FlowResult, + context: MSALNativeAuthRequestContext + ) -> MSALNativeAuthV2FlowControllerResponse { + return MSALNativeAuthV2FlowControllerResponse(result, correlationId: context.correlationId()) + } + + private func failure( + _ validated: MSALNativeAuthV2AuthorizeChallengeValidatedResponse, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext + ) -> MSALNativeAuthV2FlowControllerResponse { + let error: MSALNativeAuthFlowError + if case .error(let flowError) = validated { + error = flowError + } else { + error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected authorize-challenge response") + } + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + + private func interactionFailure( + _ validated: MSALNativeAuthV2InteractionValidatedResponse, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext, + newState: MSALNativeAuthFlowState? + ) -> MSALNativeAuthV2FlowControllerResponse { + let error: MSALNativeAuthFlowError + if case .error(let flowError) = validated { + error = flowError + } else { + error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected server response") + } + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: newState), context: context) + } + + private func notImplemented( + apiId: MSALNativeAuthTelemetryApiId, + correlationId: UUID?, + flow: String + ) -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: correlationId) + let event = makeAndStartTelemetryEvent(id: apiId, context: context) + let error = MSALNativeAuthFlowError(kind: .notImplemented, errorDescription: "\(flow) is not implemented yet.") + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift new file mode 100644 index 0000000000..ce6c3b8a58 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Drives the Native Auth V2 (server-driven, HAL) flows. +/// +/// A single unified controller backs every V2 flow. Each method performs one step +/// (or, for entry methods, the initial sequence of steps that the server can complete +/// without app interaction) and returns a ``MSALNativeAuthV2FlowControllerResponse``. +protocol MSALNativeAuthV2FlowControlling { + + // MARK: - Entry points + + func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse + + func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse + + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse + + // MARK: - Continuation + + func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + + func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + + func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + state: MSALNativeAuthFlowState + ) async -> MSALNativeAuthV2FlowControllerResponse + + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + + func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift new file mode 100644 index 0000000000..23d34ea795 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -0,0 +1,95 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Identifies which V2 flow a ``MSALNativeAuthFlowState`` belongs to. +enum MSALNativeAuthV2FlowType { + case signUp + case signIn + case resetPassword +} + +/// Internal continuation context carried by a ``MSALNativeAuthFlowState``. +/// +/// Holds the opaque server `continuation_token` and the resolved `_links` hrefs +/// the SDK must follow to advance the server-driven flow. +struct MSALNativeAuthV2ContinuationState { + let flowType: MSALNativeAuthV2FlowType + let continuationToken: String + /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue"). + let links: [String: URL] + let username: String? + let sentToHint: String? + let codeLength: Int? + + func link(_ relation: String) -> URL? { + return links[relation] + } +} + +/// Result produced by the unified V2 controller for a single step of a flow. +enum MSALNativeAuthV2FlowResult { + case actionRequired(action: MSALNativeAuthAction, newState: MSALNativeAuthFlowState) + case completed(MSALNativeAuthUserAccountResult) + case error(error: MSALNativeAuthFlowError, newState: MSALNativeAuthFlowState?) + case browserRequired(url: URL, newState: MSALNativeAuthFlowState) +} + +/// Wraps the controller result with the correlation id and an optional telemetry update closure. +struct MSALNativeAuthV2FlowControllerResponse { + let result: MSALNativeAuthV2FlowResult + let correlationId: UUID + let telemetryUpdate: ((Result) -> Void)? + + init( + _ result: MSALNativeAuthV2FlowResult, + correlationId: UUID, + telemetryUpdate: ((Result) -> Void)? = nil + ) { + self.result = result + self.correlationId = correlationId + self.telemetryUpdate = telemetryUpdate + } +} + +/// Routes a controller response to the appropriate ``MSALNativeAuthFlowDelegate`` callback. +struct MSALNativeAuthFlowResponseDispatcher { + + func dispatch(_ response: MSALNativeAuthV2FlowControllerResponse, delegate: MSALNativeAuthFlowDelegate) async { + switch response.result { + case .actionRequired(let action, let newState): + await delegate.onActionRequired(action: action, flowState: newState) + response.telemetryUpdate?(.success(())) + case .completed(let result): + await delegate.onFlowCompleted(result: result) + response.telemetryUpdate?(.success(())) + case .error(let error, let newState): + await delegate.onFlowError(error: error, flowState: newState) + case .browserRequired(let url, let newState): + await delegate.onBrowserRequired(url: url, flowState: newState) + response.telemetryUpdate?(.success(())) + } + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift new file mode 100644 index 0000000000..64eeddc0ab --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +/// Generic, server-driven HAL response used by all Native Auth V2 flows. +/// +/// Every V2 HTTP outcome (200 success, the bootstrap `401` from `authorize-challenge`, +/// and `4xx` error bodies) is parsed into a single ``MSALNativeAuthHALResponse``. The +/// V2 response validator then inspects `error`, `state` and `action` to decide how the +/// flow should proceed. HAL parsing itself is delegated to the shared +/// `MSIDHALResource` / `MSIDHALLink` types in IdentityCore. +struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { + + /// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). + struct EmbeddedMethod { + let id: String? + let type: String? + let hint: String? + /// `_links` of the embedded method, keyed by relation (e.g. "challenge", "verify"), value is the raw href. + let links: [String: String] + } + + /// A server error body (`{ "error": { ... } }`). + struct ServerError { + let code: String? + let message: String? + let innerErrorCode: String? + let correlationId: UUID? + } + + let statusCode: Int + var correlationId: UUID? + + let state: String? + let action: String? + let continuationToken: String? + let codeLength: Int? + let hint: String? + + /// Authorization code from the final `authorize-challenge` call (step 7). + let code: String? + /// Access token from the `/token` exchange (step 8). + let accessToken: String? + + /// Top-level `_links`, keyed by relation, value is the raw href string. + let links: [String: String] + /// `_embedded.methods` entries. + let methods: [EmbeddedMethod] + + let error: ServerError? + + /// Whether this response represents a server error. + var isError: Bool { + return error != nil + } + + func href(forRelation relation: String) -> String? { + return links[relation] + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift new file mode 100644 index 0000000000..431f019874 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -0,0 +1,133 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +/// Parses a raw HTTP response into a ``MSALNativeAuthHALResponse``. +/// +/// Unlike the V1 serializer (Decodable + snake_case), V2 responses are HAL+JSON and +/// every HTTP outcome carries a meaningful body, so this serializer never throws on a +/// non-200 status — it captures the status code and lets the V2 validator decide. HAL +/// `_links` / `_embedded` extraction is delegated to the shared `MSIDHALResource`. +final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSerialization { + + func responseObject(for httpResponse: HTTPURLResponse?, data: Data?, context: MSIDRequestContext?) throws -> Any { + let statusCode = httpResponse?.statusCode ?? 0 + let correlationId = MSALNativeAuthHALResponse.retrieveCorrelationIdFromHeaders(from: httpResponse) + + guard let data = data, !data.isEmpty else { + // An empty body with a success status is still a valid (terminal) response. + return MSALNativeAuthHALResponse( + statusCode: statusCode, + correlationId: correlationId, + state: nil, + action: nil, + continuationToken: nil, + codeLength: nil, + hint: nil, + code: nil, + accessToken: nil, + links: [:], + methods: [], + error: nil + ) + } + + guard let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "V2 ResponseSerializer failed: body is not a JSON object") + throw MSALNativeAuthInternalError.responseSerializationError(headerCorrelationId: correlationId) + } + + let resource = MSIDHALResource(json: json) + + return MSALNativeAuthHALResponse( + statusCode: statusCode, + correlationId: correlationId, + state: resource.string(forKey: "state"), + action: resource.string(forKey: "action"), + continuationToken: resource.string(forKey: "continuation_token"), + codeLength: json["codeLength"] as? Int, + hint: resource.string(forKey: "hint"), + code: resource.string(forKey: "code"), + accessToken: resource.string(forKey: "access_token"), + links: Self.parseLinks(from: resource), + methods: Self.parseMethods(from: resource), + error: Self.parseError(from: json, fallbackCorrelationId: correlationId) + ) + } + + private static func parseLinks(from resource: MSIDHALResource) -> [String: String] { + var result: [String: String] = [:] + for (relation, links) in resource.links { + if let href = links.first?.href { + result[relation] = href + } + } + return result + } + + private static func parseMethods(from resource: MSIDHALResource) -> [MSALNativeAuthHALResponse.EmbeddedMethod] { + let methodResources = resource.embeddedResources(forRelation: "methods") + return methodResources.map { dict in + let methodResource = MSIDHALResource(json: dict) + var links: [String: String] = [:] + for (relation, halLinks) in methodResource.links { + if let href = halLinks.first?.href { + links[relation] = href + } + } + return MSALNativeAuthHALResponse.EmbeddedMethod( + id: methodResource.string(forKey: "id"), + type: methodResource.string(forKey: "type"), + hint: methodResource.string(forKey: "hint"), + links: links + ) + } + } + + private static func parseError(from json: [String: Any], fallbackCorrelationId: UUID?) -> MSALNativeAuthHALResponse.ServerError? { + guard let errorDict = json["error"] as? [String: Any] else { + return nil + } + + var innerErrorCode: String? + if let innerError = errorDict["innerError"] as? [String: Any] { + innerErrorCode = innerError["code"] as? String + } + + var correlationId = fallbackCorrelationId + if let serverCorrelationId = errorDict["correlation_id"] as? String { + correlationId = UUID(uuidString: serverCorrelationId) ?? fallbackCorrelationId + } + + return MSALNativeAuthHALResponse.ServerError( + code: errorDict["code"] as? String, + message: errorDict["message"] as? String, + innerErrorCode: innerErrorCode, + correlationId: correlationId + ) + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift new file mode 100644 index 0000000000..7cb1b90e82 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift @@ -0,0 +1,59 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +/// Error handler for the Native Auth V2 transport pipeline. +/// +/// `MSIDHttpRequest` only routes HTTP 200 through the response serializer; every other +/// status code is delivered here. In V2 the meaningful body lives on every outcome — +/// the bootstrap `401` from `authorize-challenge` carries the `continuation_token`, and +/// `4xx` responses carry an `error` object. So this handler simply re-runs the HAL +/// response serializer for any status and hands the parsed ``MSALNativeAuthHALResponse`` +/// back to the caller; the V2 validator (not the transport) decides success vs failure. +final class MSALNativeAuthV2ResponseErrorHandler: NSObject, MSIDHttpRequestErrorHandling { + + func handleError( + _ error: Error?, + httpResponse: HTTPURLResponse?, + data: Data?, + httpRequest: MSIDHttpRequestProtocol?, + responseSerializer: MSIDResponseSerialization?, + externalSSOContext ssoContext: MSIDExternalSSOContext?, + context: MSIDRequestContext?, + completionBlock: MSIDHttpRequestDidCompleteBlock? + ) { + let serializer = responseSerializer ?? MSALNativeAuthV2HALResponseSerializer() + + do { + let responseObject = try serializer.responseObject(for: httpResponse, data: data, context: context) + completionBlock?(responseObject, nil) + } catch let serializerError { + MSALNativeAuthLogger.log(level: .error, context: context, format: "V2 error handler could not parse response body") + completionBlock?(nil, serializerError) + } + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift new file mode 100644 index 0000000000..62839a6f27 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -0,0 +1,158 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Maps a raw ``MSALNativeAuthHALResponse`` (or transport error) into a validated, controller-facing response. +protocol MSALNativeAuthV2ResponseValidating { + func validateAuthorizeChallenge(_ result: Result) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse + func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse + func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse +} + +final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidating { + + func validateAuthorizeChallenge( + _ result: Result + ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + switch result { + case .failure(let error): + return .error(Self.flowError(from: error)) + case .success(let response): + if let error = response.error { + return .error(Self.flowError(from: error)) + } + if let code = response.code { + return .authorizationCode(code: code) + } + if let continuationToken = response.continuationToken { + return .continuationToken(continuationToken: continuationToken, links: response.links) + } + return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "authorize-challenge returned neither a continuation token nor a code")) + } + } + + func validateInteraction( + _ result: Result + ) -> MSALNativeAuthV2InteractionValidatedResponse { + switch result { + case .failure(let error): + return .error(Self.flowError(from: error)) + case .success(let response): + if let error = response.error { + return .error(Self.flowError(from: error)) + } + + if response.state == "continue" { + guard let continuationToken = response.continuationToken else { + return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing continuation token in 'continue' response")) + } + return .readyToComplete(continuationToken: continuationToken) + } + + guard let continuationToken = response.continuationToken else { + return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing continuation token in interaction response")) + } + + switch response.action { + case "challenge": + let method = response.methods.first + return .challengeRequired( + continuationToken: continuationToken, + challengeHref: method?.links["challenge"], + hint: method?.hint + ) + case "verify": + return .codeRequired( + continuationToken: continuationToken, + verifyHref: response.href(forRelation: "verify"), + resendHref: response.href(forRelation: "resend"), + sentTo: response.hint ?? "", + codeLength: response.codeLength ?? 0 + ) + case "update": + return .updateRequired( + continuationToken: continuationToken, + updateHref: response.href(forRelation: "update") ?? response.href(forRelation: "self") + ) + case "poll": + return .pollInProgress( + continuationToken: continuationToken, + pollHref: response.href(forRelation: "poll") + ) + default: + return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) + } + } + } + + func validateToken( + _ result: Result + ) -> MSALNativeAuthV2TokenValidatedResponse { + switch result { + case .failure(let error): + return .error(Self.flowError(from: error)) + case .success(let response): + if let error = response.error { + return .error(Self.flowError(from: error)) + } + return .success(accessToken: response.accessToken) + } + } + + // MARK: - Error mapping + + private static func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { + let message = serverError.message + let kind: MSALNativeAuthFlowError.Kind + + if serverError.innerErrorCode == "invalidContinuationToken" { + // An invalid OTP and an invalid continuation token share the inner code; the outer + // code disambiguates (invalidGrant => the supplied OTP was wrong). + kind = serverError.code == "invalidGrant" ? .invalidCode : .invalidContinuationToken + } else if let message = message, message.contains("AADSTS50034") { + kind = .userNotFound + } else if serverError.code == "invalidGrant" { + kind = .invalidCode + } else { + kind = .generalError + } + + return MSALNativeAuthFlowError( + kind: kind, + errorDescription: message, + correlationId: serverError.correlationId + ) + } + + private static func flowError(from error: Error) -> MSALNativeAuthFlowError { + if let flowError = error as? MSALNativeAuthFlowError { + return flowError + } + return MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: (error as NSError).localizedDescription + ) + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift new file mode 100644 index 0000000000..9b69dc854a --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Validated outcome of an `authorize-challenge` call (steps 1 and 7). +enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { + /// Bootstrap (step 1): `401` carrying the continuation token and the `reset_password` link. + case continuationToken(continuationToken: String, links: [String: String]) + /// Completion (step 7): the authorization code to exchange for tokens. + case authorizationCode(code: String) + case error(MSALNativeAuthFlowError) + + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.continuationToken(lToken, lLinks), .continuationToken(rToken, rLinks)): + return lToken == rToken && lLinks == rLinks + case let (.authorizationCode(lCode), .authorizationCode(rCode)): + return lCode == rCode + case let (.error(lError), .error(rError)): + return lError.kind == rError.kind + default: + return false + } + } +} + +/// Validated outcome of an SSPR interaction step (resetpassword start / challenge / verify / update / poll). +/// +/// A single enum represents every HAL interaction response; the validator selects the case +/// from the HAL `state` / `action` pair. +enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { + /// `action == challenge`: a verification method is available; the SDK should auto-trigger the challenge. + case challengeRequired(continuationToken: String, challengeHref: String?, hint: String?) + /// `action == verify`: a one-time code is required from the user. + case codeRequired(continuationToken: String, verifyHref: String?, resendHref: String?, sentTo: String, codeLength: Int) + /// `action == update`: a new password is required from the user. + case updateRequired(continuationToken: String, updateHref: String?) + /// `action == poll`: the operation is still running; keep polling. + case pollInProgress(continuationToken: String, pollHref: String?) + /// `state == continue`: the flow is ready to complete (call `authorize-challenge`). + case readyToComplete(continuationToken: String) + case error(MSALNativeAuthFlowError) + + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.challengeRequired(lToken, lHref, lHint), .challengeRequired(rToken, rHref, rHint)): + return lToken == rToken && lHref == rHref && lHint == rHint + case let (.codeRequired(lToken, lVerify, lResend, lSent, lLen), .codeRequired(rToken, rVerify, rResend, rSent, rLen)): + return lToken == rToken && lVerify == rVerify && lResend == rResend && lSent == rSent && lLen == rLen + case let (.updateRequired(lToken, lHref), .updateRequired(rToken, rHref)): + return lToken == rToken && lHref == rHref + case let (.pollInProgress(lToken, lHref), .pollInProgress(rToken, rHref)): + return lToken == rToken && lHref == rHref + case let (.readyToComplete(lToken), .readyToComplete(rToken)): + return lToken == rToken + case let (.error(lError), .error(rError)): + return lError.kind == rError.kind + default: + return false + } + } +} + +/// Validated outcome of the `/token` exchange (step 8). +enum MSALNativeAuthV2TokenValidatedResponse { + case success(accessToken: String?) + case error(MSALNativeAuthFlowError) +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift new file mode 100644 index 0000000000..61578cc961 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Fixed (non server-driven) endpoints used by the Native Auth V2 flows. +/// +/// V2 is server-driven: most steps follow `_links` hrefs returned by the server. Only +/// the bootstrap / terminal OAuth endpoints and the SSPR entry endpoint have fixed paths +/// the client must know up-front. +enum MSALNativeAuthV2Endpoint: String, CaseIterable { + /// Bootstrap (step 1) and completion (step 7). + case authorizeChallenge = "/oauth2/v2.0/authorize-challenge" + /// Token exchange (step 8). + case token = "/oauth2/v2.0/token" + /// Self-service password reset entry (step 2). + case resetPasswordStart = "/api/v0.1/auth/resetpassword" +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift new file mode 100644 index 0000000000..33f1a6ddb4 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -0,0 +1,114 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Builds request URLs for the Native Auth V2 flows. +/// +/// V2 is server-driven, so most steps follow `_links` hrefs returned by the server. Those +/// hrefs may be absolute, or relative/templated (e.g. `{tenant}/api/v0.1/auth/...`). This +/// resolver normalises a server href against the configured authority host, and also builds +/// URLs for the fixed ``MSALNativeAuthV2Endpoint`` cases. The slice/data-center query +/// parameter is appended consistently, mirroring the V1 `makeEndpointUrl` behaviour. +struct MSALNativeAuthV2HrefURLResolver { + + private let authorityURL: URL + private let dataCenter: String? + + init(config: MSALNativeAuthInternalConfiguration) { + self.authorityURL = config.authority.url + self.dataCenter = config.sliceConfig?.dc + } + + init(authorityURL: URL, dataCenter: String?) { + self.authorityURL = authorityURL + self.dataCenter = dataCenter + } + + /// Builds the URL for a fixed V2 endpoint by appending its path to the authority. + func url(for endpoint: MSALNativeAuthV2Endpoint) throws -> URL { + guard var components = URLComponents(url: authorityURL, resolvingAgainstBaseURL: true) else { + throw MSALNativeAuthInternalError.invalidUrl + } + components.path += endpoint.rawValue + return try applyingDataCenter(to: components) + } + + /// Resolves a server-provided `_links` href into an absolute URL against the authority host. + func url(forHref href: String) throws -> URL { + let trimmed = href.trimmingCharacters(in: .whitespacesAndNewlines) + + // Absolute href: use as-is (still append the data-center parameter). + if let absolute = URL(string: trimmed), let scheme = absolute.scheme, scheme.hasPrefix("http") { + guard let components = URLComponents(url: absolute, resolvingAgainstBaseURL: false) else { + throw MSALNativeAuthInternalError.invalidUrl + } + return try applyingDataCenter(to: components) + } + + // Relative / templated href: strip any leading tenant placeholder and resolve the + // remaining path against the authority's scheme + host. + let path = normalizedPath(from: trimmed) + + guard var components = URLComponents(url: authorityURL, resolvingAgainstBaseURL: true) else { + throw MSALNativeAuthInternalError.invalidUrl + } + components.path = path + components.query = nil + return try applyingDataCenter(to: components) + } + + private func normalizedPath(from href: String) -> String { + var path = href + + // Drop a leading `{tenant}` placeholder segment if present. + if path.hasPrefix("{tenant}") { + path = String(path.dropFirst("{tenant}".count)) + } + + // Strip any leading host-like segment that is not a path (defensive: server returns + // host-relative hrefs, so anything before the first `/api` or `/oauth2` is dropped). + if !path.hasPrefix("/") { + path = "/" + path + } + + return path + } + + private func applyingDataCenter(to components: URLComponents) throws -> URL { + var components = components + if let dataCenter = dataCenter { + var queryItems = components.queryItems ?? [] + if !queryItems.contains(where: { $0.name == "dc" }) { + queryItems.append(URLQueryItem(name: "dc", value: dataCenter)) + } + components.queryItems = queryItems + } + + guard let url = components.url else { + throw MSALNativeAuthInternalError.invalidUrl + } + return url + } +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift new file mode 100644 index 0000000000..86a1b9a229 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -0,0 +1,199 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +/// Builds the `MSIDHttpRequest` objects for each step of the Native Auth V2 flows. +protocol MSALNativeAuthV2RequestProviding { + + /// Step 1: bootstrap `authorize-challenge` (no continuation token) → `401` + continuation token. + func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 7: completion `authorize-challenge` (with continuation token) → authorization code. + func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 8: token exchange. + func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 2: SSPR entry (fixed endpoint). + func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 3: send EOTP (server `challenge` / `resend` href). + func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 4: verify OTP (server `verify` href). + func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 5: update password (server `update` href, PUT). + func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Step 6: poll for completion (server `poll` href). + func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest +} + +final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { + + private let config: MSALNativeAuthInternalConfiguration + private let resolver: MSALNativeAuthV2HrefURLResolver + private let defaultScope = "openid offline_access profile" + + init(config: MSALNativeAuthInternalConfiguration) { + self.config = config + self.resolver = MSALNativeAuthV2HrefURLResolver(config: config) + } + + func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(for: .authorizeChallenge) + return makeRequest(url: url, method: "POST", form: [ + "client_id": config.clientId, + "scope": defaultScope + ], context: context) + } + + func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(for: .authorizeChallenge) + return makeRequest(url: url, method: "POST", form: [ + "client_id": config.clientId, + "scope": defaultScope, + "continuation_token": continuationToken + ], context: context) + } + + func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(for: .token) + return makeRequest(url: url, method: "POST", form: [ + "grant_type": "authorization_code", + "code": code, + "client_id": config.clientId, + "scope": defaultScope + ], context: context) + } + + func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(for: .resetPasswordStart) + return makeRequest(url: url, method: "POST", json: [ + "username": username, + "continuation_token": continuationToken + ], context: context) + } + + func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + return makeRequest(url: url, method: "POST", json: [ + "continuation_token": continuationToken + ], context: context) + } + + func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + return makeRequest(url: url, method: "POST", json: [ + "otp": otp, + "continuation_token": continuationToken + ], context: context) + } + + func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + return makeRequest(url: url, method: "PUT", json: [ + "new_password": newPassword, + "continuation_token": continuationToken + ], context: context) + } + + func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + return makeRequest(url: url, method: "POST", json: [ + "continuation_token": continuationToken + ], context: context) + } + + // MARK: - Request building + + private func makeRequest( + url: URL, + method: String, + json: [String: Any]? = nil, + form: [String: String]? = nil, + context: MSALNativeAuthRequestContext + ) -> MSIDHttpRequest { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = method + + if let json = json { + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: json) + } else if let form = form { + urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + urlRequest.httpBody = Self.encodeForm(form).data(using: .utf8) + } + + urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") + urlRequest.setValue(context.correlationId().uuidString, forHTTPHeaderField: "client-request-id") + + let request = MSIDHttpRequest() + request.urlRequest = urlRequest + request.context = context + request.responseSerializer = MSALNativeAuthV2HALResponseSerializer() + request.errorHandler = MSALNativeAuthV2ResponseErrorHandler() + + if let interceptor = config.requestInterceptor { + request.requestInterceptor = MSALNativeAuthV2RequestInterceptorBridge(interceptor: interceptor) + } + + return request + } + + private static func encodeForm(_ parameters: [String: String]) -> String { + var allowed = CharacterSet.urlQueryAllowed + allowed.remove(charactersIn: "+&=") + return parameters + .map { key, value in + let encodedKey = key.addingPercentEncoding(withAllowedCharacters: allowed) ?? key + let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value + return "\(encodedKey)=\(encodedValue)" + } + .joined(separator: "&") + } +} + +/// Bridges `MSALNativeAuthRequestInterceptor` (Swift public protocol) to `MSIDHttpRequestInterceptorProtocol` (ObjC). +private final class MSALNativeAuthV2RequestInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { + + private let interceptor: MSALNativeAuthRequestInterceptor + + init(interceptor: MSALNativeAuthRequestInterceptor) { + self.interceptor = interceptor + } + + func addAdditionalHeaderFields( + for requestUrl: URL?, + with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock + ) { + interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in + completionBlock(additionalHeaders) + } + } +} diff --git a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index 27dbf4e7d7..c3908ee8c5 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -152,6 +152,7 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic /// - Parameters: /// - parameters: Parameters used for the Sign Up flow. /// - delegate: Delegate that receives callbacks for the Sign Up flow. + @available(*, deprecated, message: "Use signUpV2(parameters:delegate:) instead") public func signUp( parameters: MSALNativeAuthSignUpParameters, delegate: SignUpStartDelegate @@ -187,6 +188,7 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic /// - Parameters: /// - parameters: Parameters used for the Sign In flow. /// - delegate: Delegate that receives callbacks for the Sign In flow. + @available(*, deprecated, message: "Use signInV2(parameters:delegate:) instead") public func signIn( parameters: MSALNativeAuthSignInParameters, delegate: SignInStartDelegate @@ -230,10 +232,11 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic } } - /// Reset the password using parameters + /// Reset the password using parameters. /// - Parameters: /// - parameters: Parameters used for the Reset Password flow. /// - delegate: Delegate that receives callbacks for the Reset Password flow. + @available(*, deprecated, message: "Use resetPasswordV2(parameters:delegate:) instead") public func resetPassword( parameters: MSALNativeAuthResetPasswordParameters, delegate: ResetPasswordStartDelegate @@ -259,6 +262,56 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic } } + // MARK: - Native Auth V2 (server-driven) + + /// Reset the password using the server-driven (V2) flow. + /// - Parameters: + /// - parameters: Parameters used for the Reset Password flow. + /// - delegate: Unified delegate that receives callbacks for the flow. + public func resetPasswordV2( + parameters: MSALNativeAuthResetPasswordParameters, + delegate: MSALNativeAuthFlowDelegate + ) { + let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + Task { + let response = await controller.resetPassword(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) + } + } + + /// Sign up a user using the server-driven (V2) flow. + /// - Parameters: + /// - parameters: Parameters used for the Sign Up flow. + /// - delegate: Unified delegate that receives callbacks for the flow. + public func signUpV2( + parameters: MSALNativeAuthSignUpParameters, + delegate: MSALNativeAuthFlowDelegate + ) { + let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + Task { + let response = await controller.signUp(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) + } + } + + /// Sign in a user using the server-driven (V2) flow. + /// - Parameters: + /// - parameters: Parameters used for the Sign In flow. + /// - delegate: Unified delegate that receives callbacks for the flow. + public func signInV2( + parameters: MSALNativeAuthSignInParameters, + delegate: MSALNativeAuthFlowDelegate + ) { + let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + Task { + let response = await controller.signIn(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) + } + } + /// Retrieve the current signed in account from the cache. /// - Parameter correlationId: Optional. UUID to correlate this request with the server for debugging. /// - Returns: An object representing the account information if present in the local cache. diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift new file mode 100644 index 0000000000..5c50ed9935 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Actions the server can request during a Native Auth V2 (server-driven) flow. +/// +/// In V2 the server drives the flow: each step the SDK reports an +/// ``MSALNativeAuthAction`` through ``MSALNativeAuthFlowDelegate/onActionRequired(action:flowState:)`` +/// and the app continues by calling the corresponding method on the supplied +/// ``MSALNativeAuthFlowState``. +public enum MSALNativeAuthAction { + + /// The server requires the user to verify a one-time code. + /// Continue with ``MSALNativeAuthFlowState/submitCode(_:delegate:)``. + case codeRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) + + /// The server requires the user to enter their password. + /// Continue with ``MSALNativeAuthFlowState/submitPassword(_:delegate:)``. + case passwordRequired + + /// The server requires the user to enter a new password (self-service password reset). + /// Continue with ``MSALNativeAuthFlowState/submitNewPassword(_:delegate:)``. + case newPasswordRequired + + /// The server requires additional user attributes. + /// Continue with ``MSALNativeAuthFlowState/submitAttributes(_:delegate:)``. + case attributesRequired(attributes: [MSALNativeAuthRequiredAttribute]) + + /// The server reports that some attributes were invalid and must be corrected. + case attributesInvalid(attributeNames: [String]) + + /// The server requires multi-factor authentication; the user must select an auth method. + /// Continue with ``MSALNativeAuthFlowState/selectAuthMethod(_:verificationContact:delegate:)``. + case mfaRequired(authMethods: [MSALAuthMethod]) + + /// The server sent an MFA challenge; the user must enter the verification code. + /// Continue with ``MSALNativeAuthFlowState/submitChallenge(_:delegate:)``. + case mfaVerificationRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) + + /// The server requires strong authentication registration (JIT); the user must select an auth method. + case strongAuthRegistrationRequired(authMethods: [MSALAuthMethod]) + + /// The server sent a JIT challenge; the user must enter the verification code. + case strongAuthVerificationRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift new file mode 100644 index 0000000000..237452fe57 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Single unified delegate for all Native Auth V2 (server-driven) flows. +/// +/// Unlike V1 — which exposes a different delegate protocol per step — V2 uses one +/// delegate for sign up, sign in and reset password. The SDK drives the flow and +/// reports back through these callbacks; the app reacts and continues the flow by +/// calling methods on the provided ``MSALNativeAuthFlowState``. +/// +/// All callbacks are invoked on the main actor. +public protocol MSALNativeAuthFlowDelegate: AnyObject { + + /// The server requires the user to perform an action before the flow can continue. + /// - Parameters: + /// - action: The action the server is requesting. + /// - flowState: Opaque handle used to continue the flow. + @MainActor func onActionRequired(action: MSALNativeAuthAction, flowState: MSALNativeAuthFlowState) + + /// The flow completed successfully and the user now has tokens. + /// - Parameter result: The authenticated user account result. + @MainActor func onFlowCompleted(result: MSALNativeAuthUserAccountResult) + + /// The flow encountered an error. + /// - Parameters: + /// - error: The error that occurred. + /// - flowState: Opaque handle used to retry/continue the flow, when the error is recoverable. + @MainActor func onFlowError(error: MSALNativeAuthFlowError, flowState: MSALNativeAuthFlowState?) + + /// The server requires the flow to continue in a web browser (e.g. an unsupported scenario). + /// - Parameters: + /// - url: The URL to open in a browser. + /// - flowState: Opaque handle used to continue the flow. + @MainActor func onBrowserRequired(url: URL, flowState: MSALNativeAuthFlowState) +} + +/// Default implementation makes ``onBrowserRequired(url:flowState:)`` optional. +public extension MSALNativeAuthFlowDelegate { + + @MainActor func onBrowserRequired(url: URL, flowState: MSALNativeAuthFlowState) { + onFlowError( + error: MSALNativeAuthFlowError( + kind: .browserRequired, + errorDescription: "The flow requires a web browser, but onBrowserRequired(url:flowState:) is not implemented." + ), + flowState: flowState + ) + } +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift new file mode 100644 index 0000000000..781201f45f --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift @@ -0,0 +1,98 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Unified error type surfaced by the Native Auth V2 (server-driven) flows. +/// +/// A single error type is used across all V2 flows so that an app's +/// ``MSALNativeAuthFlowDelegate`` has only one error shape to inspect, mirroring +/// the unified delegate contract described in the V2 interface. +@objcMembers +public class MSALNativeAuthFlowError: NSObject, LocalizedError { + + /// High level classification of the V2 flow error. + public enum Kind: Int { + /// The requested flow (or one of its steps) is not implemented yet. + case notImplemented + /// The provided username was not accepted by the server (e.g. AADSTS50034 user not found). + case userNotFound + /// The submitted one-time code was invalid or expired. + case invalidCode + /// The continuation token was rejected by the server (wrong endpoint, tampered or expired). + case invalidContinuationToken + /// The submitted password did not meet the server's requirements. + case invalidPassword + /// The username supplied to the SDK failed local validation. + case invalidUsername + /// The flow must continue in a web browser. + case browserRequired + /// A generic / unexpected error occurred. + case generalError + } + + /// The classification of this error. + public let kind: Kind + + /// A developer-facing description of the error. + public let errorDescription: String? + + /// Server error codes associated with this error, when available. + public let errorCodes: [Int] + + /// UUID correlating this error with the server logs, when available. + public let correlationId: UUID? + + init( + kind: Kind, + errorDescription: String? = nil, + errorCodes: [Int] = [], + correlationId: UUID? = nil + ) { + self.kind = kind + self.errorDescription = errorDescription + self.errorCodes = errorCodes + self.correlationId = correlationId + } + + /// Whether the flow that produced this error is not implemented yet. + public var isNotImplemented: Bool { + return kind == .notImplemented + } + + /// Whether the submitted one-time code was invalid. + public var isInvalidCode: Bool { + return kind == .invalidCode + } + + /// Whether the username was not found in the directory. + public var isUserNotFound: Bool { + return kind == .userNotFound + } + + /// Whether the continuation token was rejected by the server. + public var isInvalidContinuationToken: Bool { + return kind == .invalidContinuationToken + } +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift new file mode 100644 index 0000000000..ea251a4abd --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift @@ -0,0 +1,105 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Opaque handle that lets an app continue a Native Auth V2 (server-driven) flow. +/// +/// The SDK hands a ``MSALNativeAuthFlowState`` to the app via +/// ``MSALNativeAuthFlowDelegate/onActionRequired(action:flowState:)``. The app then +/// calls the method matching the requested ``MSALNativeAuthAction`` to advance the flow. +public class MSALNativeAuthFlowState { + + let continuation: MSALNativeAuthV2ContinuationState + private let controller: MSALNativeAuthV2FlowControlling + private let dispatcher = MSALNativeAuthFlowResponseDispatcher() + + init(continuation: MSALNativeAuthV2ContinuationState, controller: MSALNativeAuthV2FlowControlling) { + self.continuation = continuation + self.controller = controller + } + + /// Submit a one-time verification code. + public func submitCode(_ code: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller in + await controller.submitCode(code, state: self) + } + } + + /// Submit a password (sign in / sign up). + public func submitPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller in + await controller.submitPassword(password, state: self) + } + } + + /// Submit a new password (self-service password reset). + public func submitNewPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller in + await controller.submitNewPassword(password, state: self) + } + } + + /// Submit user attributes (sign up). + public func submitAttributes(_ attributes: [String: Any], delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller in + await controller.submitAttributes(attributes, state: self) + } + } + + /// Select an authentication method for MFA or strong-auth registration. + public func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String? = nil, + delegate: MSALNativeAuthFlowDelegate + ) { + run(delegate: delegate) { controller in + await controller.selectAuthMethod(method, verificationContact: verificationContact, state: self) + } + } + + /// Submit an MFA / strong-auth challenge response. + public func submitChallenge(_ challenge: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller in + await controller.submitChallenge(challenge, state: self) + } + } + + /// Request the server to resend the one-time code. + public func resendCode(delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller in + await controller.resendCode(state: self) + } + } + + private func run( + delegate: MSALNativeAuthFlowDelegate, + operation: @escaping (MSALNativeAuthV2FlowControlling) async -> MSALNativeAuthV2FlowControllerResponse + ) { + Task { + let response = await operation(controller) + await dispatcher.dispatch(response, delegate: delegate) + } + } +} diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift new file mode 100644 index 0000000000..0fbb666970 --- /dev/null +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -0,0 +1,265 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { + + private var sut: MSALNativeAuthV2FlowController! + private var requestProviderMock: MSALNativeAuthV2RequestProviderMock! + private var validatorMock: MSALNativeAuthV2ResponseValidatorMock! + private var cacheAccessorMock: MSALNativeAuthCacheAccessorMock! + + override func setUpWithError() throws { + try super.setUpWithError() + + requestProviderMock = .init() + validatorMock = .init() + cacheAccessorMock = .init() + + sut = .init( + config: MSALNativeAuthConfigStubs.configuration, + requestProvider: requestProviderMock, + responseValidator: validatorMock, + cacheAccessor: cacheAccessorMock + ) + } + + // MARK: - Helpers + + private func makeState(links: [String: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowState { + let continuation = MSALNativeAuthV2ContinuationState( + flowType: .resetPassword, + continuationToken: continuationToken, + links: links, + username: "user@contoso.com", + sentToHint: "u***@contoso.com", + codeLength: 8 + ) + return MSALNativeAuthFlowState(continuation: continuation, controller: sut) + } + + private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParameters { + let params = MSALNativeAuthResetPasswordParameters(username: "user@contoso.com") + return params + } + + // MARK: - resetPassword (happy path -> code required) + + func test_resetPassword_happyPath_returnsCodeRequired() async { + requestProviderMock.mockRequest() + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-bootstrap", links: ["reset_password": "https://contoso.com/reset"]) + ] + validatorMock.interactionResponses = [ + .challengeRequired(continuationToken: "ct-2", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com"), + .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + ] + + let response = await sut.resetPassword(parameters: resetPasswordParameters()) + + guard case .actionRequired(let action, _) = response.result else { + return XCTFail("Expected actionRequired, got \(response.result)") + } + guard case .codeRequired = action else { + return XCTFail("Expected codeRequired action, got \(action)") + } + XCTAssertTrue(requestProviderMock.authorizeChallengeStartCalled) + XCTAssertTrue(requestProviderMock.resetPasswordStartCalled) + XCTAssertTrue(requestProviderMock.challengeCalled) + } + + func test_resetPassword_whenBootstrapFails_returnsError() async { + requestProviderMock.mockRequest() + validatorMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(kind: .generalError))] + + let response = await sut.resetPassword(parameters: resetPasswordParameters()) + + guard case .error = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertFalse(requestProviderMock.resetPasswordStartCalled) + } + + func test_resetPassword_whenUserNotFound_returnsError() async { + requestProviderMock.mockRequest() + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-bootstrap", links: [:]) + ] + validatorMock.interactionResponses = [ + .error(MSALNativeAuthFlowError(kind: .userNotFound)) + ] + + let response = await sut.resetPassword(parameters: resetPasswordParameters()) + + guard case .error(let error, _) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertTrue(error.isUserNotFound) + } + + // MARK: - submitCode + + func test_submitCode_whenUpdateRequired_returnsNewPasswordRequired() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [ + .updateRequired(continuationToken: "ct-update", updateHref: "https://contoso.com/update") + ] + let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) + + let response = await sut.submitCode("12345678", state: state) + + guard case .actionRequired(let action, _) = response.result else { + return XCTFail("Expected actionRequired, got \(response.result)") + } + guard case .newPasswordRequired = action else { + return XCTFail("Expected newPasswordRequired action, got \(action)") + } + XCTAssertTrue(requestProviderMock.verifyCalled) + XCTAssertEqual(requestProviderMock.verifyHrefReceived, "https://contoso.com/verify") + } + + func test_submitCode_whenInvalidCode_returnsErrorWithRetryState() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [ + .error(MSALNativeAuthFlowError(kind: .invalidCode)) + ] + let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) + + let response = await sut.submitCode("00000000", state: state) + + guard case .error(let error, let newState) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertTrue(error.isInvalidCode) + XCTAssertNotNil(newState) + } + + func test_submitCode_whenVerifyLinkMissing_returnsError() async { + requestProviderMock.mockRequest() + let state = makeState(links: [:]) + + let response = await sut.submitCode("12345678", state: state) + + guard case .error = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertFalse(requestProviderMock.verifyCalled) + } + + // MARK: - submitNewPassword (poll -> token -> completed) + + func test_submitNewPassword_happyPath_returnsCompleted() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [ + .pollInProgress(continuationToken: "ct-poll", pollHref: "https://contoso.com/poll"), + .readyToComplete(continuationToken: "ct-continue") + ] + validatorMock.authorizeChallengeResponses = [ + .authorizationCode(code: "auth-code") + ] + validatorMock.tokenResponse = .success(accessToken: "access-token") + let state = makeState(links: ["update": URL(string: "https://contoso.com/update")!]) + + let response = await sut.submitNewPassword("New-Password-1", state: state) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") + } + XCTAssertTrue(requestProviderMock.updatePasswordCalled) + XCTAssertTrue(requestProviderMock.pollCalled) + XCTAssertTrue(requestProviderMock.tokenCalled) + } + + func test_submitNewPassword_whenUpdateLinkMissing_returnsError() async { + requestProviderMock.mockRequest() + let state = makeState(links: [:]) + + let response = await sut.submitNewPassword("New-Password-1", state: state) + + guard case .error = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertFalse(requestProviderMock.updatePasswordCalled) + } + + // MARK: - resendCode + + func test_resendCode_whenCodeRequired_returnsCodeRequired() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [ + .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + ] + let state = makeState(links: ["resend": URL(string: "https://contoso.com/resend")!]) + + let response = await sut.resendCode(state: state) + + guard case .actionRequired(let action, _) = response.result else { + return XCTFail("Expected actionRequired, got \(response.result)") + } + guard case .codeRequired = action else { + return XCTFail("Expected codeRequired action, got \(action)") + } + XCTAssertTrue(requestProviderMock.challengeCalled) + } + + // MARK: - notImplemented flows + + func test_signUp_returnsNotImplemented() async { + let response = await sut.signUp(parameters: MSALNativeAuthSignUpParameters(username: "user@contoso.com")) + assertNotImplemented(response) + } + + func test_signIn_returnsNotImplemented() async { + let response = await sut.signIn(parameters: MSALNativeAuthSignInParameters(username: "user@contoso.com")) + assertNotImplemented(response) + } + + func test_submitPassword_returnsNotImplemented() async { + let state = makeState(links: [:]) + let response = await sut.submitPassword("password", state: state) + assertNotImplemented(response) + } + + func test_submitAttributes_returnsNotImplemented() async { + let state = makeState(links: [:]) + let response = await sut.submitAttributes([:], state: state) + assertNotImplemented(response) + } + + func test_submitChallenge_returnsNotImplemented() async { + let state = makeState(links: [:]) + let response = await sut.submitChallenge("challenge", state: state) + assertNotImplemented(response) + } + + private func assertNotImplemented(_ response: MSALNativeAuthV2FlowControllerResponse) { + guard case .error(let error, _) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertTrue(error.isNotImplemented) + } +} diff --git a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift index 8d5de7eaf1..29f461b9a1 100644 --- a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift +++ b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift @@ -80,6 +80,7 @@ class MSALNativeAuthControllerFactoryMock: MSALNativeAuthControllerBuildable { var jitController = MSALNativeAuthJITControllerMock() var resetPasswordController = MSALNativeAuthResetPasswordControllerMock() var credentialsController = MSALNativeAuthCredentialsControllerMock() + var v2FlowController = MSALNativeAuthV2FlowControllerMock() func makeSignUpController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthSignUpControlling { return signUpController @@ -100,6 +101,10 @@ class MSALNativeAuthControllerFactoryMock: MSALNativeAuthControllerBuildable { func makeCredentialsController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthCredentialsControlling { return credentialsController } + + func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthV2FlowControlling { + return v2FlowController + } } class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuildable { @@ -109,17 +114,20 @@ class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuild var jitController: MSALNativeAuthJITControlling! var resetPasswordController: MSALNativeAuthResetPasswordControlling! var credentialsController: MSALNativeAuthCredentialsControlling! + var v2FlowController: MSALNativeAuthV2FlowControlling! init (signUpController: MSALNativeAuthSignUpControlling = MSALNativeAuthSignUpControllerMock(), signInController: MSALNativeAuthSignInControlling = MSALNativeAuthSignInControllerMock(), jitController: MSALNativeAuthJITControlling = MSALNativeAuthJITControllerMock(), resetPasswordController: MSALNativeAuthResetPasswordControlling = MSALNativeAuthResetPasswordControllerMock(), - credentialsController: MSALNativeAuthCredentialsControlling = MSALNativeAuthCredentialsControllerMock()) { + credentialsController: MSALNativeAuthCredentialsControlling = MSALNativeAuthCredentialsControllerMock(), + v2FlowController: MSALNativeAuthV2FlowControlling = MSALNativeAuthV2FlowControllerMock()) { self.signUpController = signUpController self.signInController = signInController self.jitController = jitController self.resetPasswordController = resetPasswordController self.credentialsController = credentialsController + self.v2FlowController = v2FlowController } func makeSignUpController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthSignUpControlling { @@ -141,6 +149,10 @@ class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuild func makeCredentialsController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthCredentialsControlling { return credentialsController } + + func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthV2FlowControlling { + return v2FlowController + } } class MSALNativeAuthCacheAccessorFactoryMock: MSALNativeAuthCacheAccessorBuildable { diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift new file mode 100644 index 0000000000..585e0e0278 --- /dev/null +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +@testable import MSAL + +class MSALNativeAuthV2FlowControllerMock: MSALNativeAuthV2FlowControlling { + + var correlationId = UUID() + var resetPasswordResponse: MSALNativeAuthV2FlowControllerResponse? + var signUpResponse: MSALNativeAuthV2FlowControllerResponse? + var signInResponse: MSALNativeAuthV2FlowControllerResponse? + var submitCodeResponse: MSALNativeAuthV2FlowControllerResponse? + var submitPasswordResponse: MSALNativeAuthV2FlowControllerResponse? + var submitNewPasswordResponse: MSALNativeAuthV2FlowControllerResponse? + var submitAttributesResponse: MSALNativeAuthV2FlowControllerResponse? + var selectAuthMethodResponse: MSALNativeAuthV2FlowControllerResponse? + var submitChallengeResponse: MSALNativeAuthV2FlowControllerResponse? + var resendCodeResponse: MSALNativeAuthV2FlowControllerResponse? + + private func notImplementedResponse() -> MSALNativeAuthV2FlowControllerResponse { + return MSALNativeAuthV2FlowControllerResponse( + .error(error: MSALNativeAuthFlowError(kind: .notImplemented), newState: nil), + correlationId: correlationId + ) + } + + func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { + return resetPasswordResponse ?? notImplementedResponse() + } + + func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { + return signUpResponse ?? notImplementedResponse() + } + + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { + return signInResponse ?? notImplementedResponse() + } + + func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return submitCodeResponse ?? notImplementedResponse() + } + + func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return submitPasswordResponse ?? notImplementedResponse() + } + + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return submitNewPasswordResponse ?? notImplementedResponse() + } + + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return submitAttributesResponse ?? notImplementedResponse() + } + + func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + state: MSALNativeAuthFlowState + ) async -> MSALNativeAuthV2FlowControllerResponse { + return selectAuthMethodResponse ?? notImplementedResponse() + } + + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return submitChallengeResponse ?? notImplementedResponse() + } + + func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + return resendCodeResponse ?? notImplementedResponse() + } +} diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift new file mode 100644 index 0000000000..53163825c6 --- /dev/null +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { + + var throwError = false + + private(set) var authorizeChallengeStartCalled = false + private(set) var authorizeChallengeContinueCalled = false + private(set) var tokenCalled = false + private(set) var resetPasswordStartCalled = false + private(set) var challengeCalled = false + private(set) var verifyCalled = false + private(set) var updatePasswordCalled = false + private(set) var pollCalled = false + + private(set) var challengeHrefReceived: String? + private(set) var verifyHrefReceived: String? + private(set) var updateHrefReceived: String? + private(set) var pollHrefReceived: String? + + func mockRequest(throwError: Bool = false) { + self.throwError = throwError + } + + private func resolveRequest() throws -> MSIDHttpRequest { + if throwError { + throw ErrorMock.error + } + // A fresh stubbed request per call queues its own MSIDTestURLSession response, + // so flows that perform multiple sends each find a matching response. + return MSALNativeAuthHTTPRequestMock.prepareMockRequest() + } + + func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + authorizeChallengeStartCalled = true + return try resolveRequest() + } + + func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + authorizeChallengeContinueCalled = true + return try resolveRequest() + } + + func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + tokenCalled = true + return try resolveRequest() + } + + func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + resetPasswordStartCalled = true + return try resolveRequest() + } + + func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + challengeCalled = true + challengeHrefReceived = href + return try resolveRequest() + } + + func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + verifyCalled = true + verifyHrefReceived = href + return try resolveRequest() + } + + func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + updatePasswordCalled = true + updateHrefReceived = href + return try resolveRequest() + } + + func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + pollCalled = true + pollHrefReceived = href + return try resolveRequest() + } +} diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift new file mode 100644 index 0000000000..6afa72abb6 --- /dev/null +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift @@ -0,0 +1,58 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +@testable import MSAL + +class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating { + + var authorizeChallengeResponses: [MSALNativeAuthV2AuthorizeChallengeValidatedResponse] = [] + var interactionResponses: [MSALNativeAuthV2InteractionValidatedResponse] = [] + var tokenResponse: MSALNativeAuthV2TokenValidatedResponse = .error(MSALNativeAuthFlowError(kind: .generalError)) + + private(set) var validateAuthorizeChallengeCallCount = 0 + private(set) var validateInteractionCallCount = 0 + private(set) var validateTokenCallCount = 0 + + func validateAuthorizeChallenge(_ result: Result) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + defer { validateAuthorizeChallengeCallCount += 1 } + if validateAuthorizeChallengeCallCount < authorizeChallengeResponses.count { + return authorizeChallengeResponses[validateAuthorizeChallengeCallCount] + } + return .error(MSALNativeAuthFlowError(kind: .generalError)) + } + + func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse { + defer { validateInteractionCallCount += 1 } + if validateInteractionCallCount < interactionResponses.count { + return interactionResponses[validateInteractionCallCount] + } + return .error(MSALNativeAuthFlowError(kind: .generalError)) + } + + func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse { + validateTokenCallCount += 1 + return tokenResponse + } +} diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift new file mode 100644 index 0000000000..f5960b4814 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -0,0 +1,176 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL + +final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { + + private var sut: MSALNativeAuthV2ResponseValidator! + + override func setUp() { + super.setUp() + sut = MSALNativeAuthV2ResponseValidator() + } + + // MARK: - Builders + + private func makeResponse( + statusCode: Int = 200, + state: String? = nil, + action: String? = nil, + continuationToken: String? = nil, + codeLength: Int? = nil, + hint: String? = nil, + code: String? = nil, + accessToken: String? = nil, + links: [String: String] = [:], + methods: [MSALNativeAuthHALResponse.EmbeddedMethod] = [], + error: MSALNativeAuthHALResponse.ServerError? = nil + ) -> MSALNativeAuthHALResponse { + return MSALNativeAuthHALResponse( + statusCode: statusCode, + correlationId: nil, + state: state, + action: action, + continuationToken: continuationToken, + codeLength: codeLength, + hint: hint, + code: code, + accessToken: accessToken, + links: links, + methods: methods, + error: error + ) + } + + // MARK: - validateAuthorizeChallenge + + func test_validateAuthorizeChallenge_withContinuationToken() { + let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) + let result = sut.validateAuthorizeChallenge(.success(response)) + XCTAssertEqual(result, .continuationToken(continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"])) + } + + func test_validateAuthorizeChallenge_withAuthorizationCode() { + let response = makeResponse(code: "auth-code") + let result = sut.validateAuthorizeChallenge(.success(response)) + XCTAssertEqual(result, .authorizationCode(code: "auth-code")) + } + + func test_validateAuthorizeChallenge_withServerError_returnsError() { + let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad", innerErrorCode: nil, correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateAuthorizeChallenge(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + } + + func test_validateAuthorizeChallenge_withTransportFailure_returnsError() { + let result = sut.validateAuthorizeChallenge(.failure(ErrorMock.error)) + guard case .error = result else { + return XCTFail("Expected error") + } + } + + // MARK: - validateInteraction + + func test_validateInteraction_challengeAction_returnsChallengeRequired() { + let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) + let response = makeResponse(state: "interactionRequired", action: "challenge", continuationToken: "ct", methods: [method]) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .challengeRequired(continuationToken: "ct", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com")) + } + + func test_validateInteraction_verifyAction_returnsCodeRequired() { + let response = makeResponse( + state: "interactionRequired", + action: "verify", + continuationToken: "ct", + codeLength: 8, + hint: "u***@contoso.com", + links: ["verify": "https://contoso.com/verify", "resend": "https://contoso.com/resend"] + ) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8)) + } + + func test_validateInteraction_updateAction_returnsUpdateRequired() { + let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct", links: ["update": "https://contoso.com/update"]) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .updateRequired(continuationToken: "ct", updateHref: "https://contoso.com/update")) + } + + func test_validateInteraction_pollAction_returnsPollInProgress() { + let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct", links: ["poll": "https://contoso.com/poll"]) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .pollInProgress(continuationToken: "ct", pollHref: "https://contoso.com/poll")) + } + + func test_validateInteraction_continueState_returnsReadyToComplete() { + let response = makeResponse(state: "continue", continuationToken: "ct") + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .readyToComplete(continuationToken: "ct")) + } + + func test_validateInteraction_userNotFound_mapsToUserNotFound() { + let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "AADSTS50034 user not found", innerErrorCode: nil, correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .userNotFound))) + } + + func test_validateInteraction_invalidGrant_mapsToInvalidCode() { + let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "wrong code", innerErrorCode: nil, correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .invalidCode))) + } + + func test_validateInteraction_invalidContinuationToken_mapsCorrectly() { + let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad token", innerErrorCode: "invalidContinuationToken", correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .invalidContinuationToken))) + } + + // MARK: - validateToken + + func test_validateToken_success() { + let response = makeResponse(accessToken: "access-token") + let result = sut.validateToken(.success(response)) + guard case .success(let accessToken) = result else { + return XCTFail("Expected success") + } + XCTAssertEqual(accessToken, "access-token") + } + + func test_validateToken_withServerError_returnsError() { + let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "bad", innerErrorCode: nil, correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateToken(.success(response)) + guard case .error = result else { + return XCTFail("Expected error") + } + } +} From 51e062646fd41b5e2e6bd06ebc21d2e77f0cf17e Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 30 Jun 2026 15:14:39 +0100 Subject: [PATCH 02/85] hal responses dc issues url encoding --- ...SALNativeAuthV2HALResponseSerializer.swift | 2 +- .../v2/MSALNativeAuthV2HrefURLResolver.swift | 32 +++++++++++-------- .../v2/MSALNativeAuthV2RequestProvider.swift | 23 +++++++------ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 431f019874..0c0fad9aff 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -68,7 +68,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali correlationId: correlationId, state: resource.string(forKey: "state"), action: resource.string(forKey: "action"), - continuationToken: resource.string(forKey: "continuation_token"), + continuationToken: resource.string(forKey: "continuationToken") ?? resource.string(forKey: "continuation_token"), codeLength: json["codeLength"] as? Int, hint: resource.string(forKey: "hint"), code: resource.string(forKey: "code"), diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 33f1a6ddb4..3a4e08edd2 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -67,33 +67,37 @@ struct MSALNativeAuthV2HrefURLResolver { return try applyingDataCenter(to: components) } - // Relative / templated href: strip any leading tenant placeholder and resolve the - // remaining path against the authority's scheme + host. - let path = normalizedPath(from: trimmed) + // Relative / templated href: the href may already carry its own query string + // (e.g. `?dc=...`), so parse it with URLComponents to separate path from query + // rather than folding the query into the path. Strip any leading tenant + // placeholder and resolve the path against the authority's scheme + host. + guard let hrefComponents = URLComponents(string: normalizedHref(from: trimmed)) else { + throw MSALNativeAuthInternalError.invalidUrl + } guard var components = URLComponents(url: authorityURL, resolvingAgainstBaseURL: true) else { throw MSALNativeAuthInternalError.invalidUrl } - components.path = path - components.query = nil + components.path = hrefComponents.path + components.percentEncodedQuery = hrefComponents.percentEncodedQuery return try applyingDataCenter(to: components) } - private func normalizedPath(from href: String) -> String { - var path = href + private func normalizedHref(from href: String) -> String { + var result = href // Drop a leading `{tenant}` placeholder segment if present. - if path.hasPrefix("{tenant}") { - path = String(path.dropFirst("{tenant}".count)) + if result.hasPrefix("{tenant}") { + result = String(result.dropFirst("{tenant}".count)) } - // Strip any leading host-like segment that is not a path (defensive: server returns - // host-relative hrefs, so anything before the first `/api` or `/oauth2` is dropped). - if !path.hasPrefix("/") { - path = "/" + path + // The server returns host-relative hrefs; ensure a leading slash so URLComponents + // parses the leading segment as a path rather than a scheme/host. + if !result.hasPrefix("/") { + result = "/" + result } - return path + return result } private func applyingDataCenter(to components: URLComponents) throws -> URL { diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 86a1b9a229..0cb9927560 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -96,14 +96,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { let url = try resolver.url(for: .resetPasswordStart) return makeRequest(url: url, method: "POST", json: [ "username": username, - "continuation_token": continuationToken + "continuationToken": continuationToken ], context: context) } func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(forHref: href) return makeRequest(url: url, method: "POST", json: [ - "continuation_token": continuationToken + "continuationToken": continuationToken ], context: context) } @@ -111,22 +111,22 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { let url = try resolver.url(forHref: href) return makeRequest(url: url, method: "POST", json: [ "otp": otp, - "continuation_token": continuationToken + "continuationToken": continuationToken ], context: context) } func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(forHref: href) return makeRequest(url: url, method: "PUT", json: [ - "new_password": newPassword, - "continuation_token": continuationToken + "newPassword": newPassword, + "continuationToken": continuationToken ], context: context) } func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(forHref: href) return makeRequest(url: url, method: "POST", json: [ - "continuation_token": continuationToken + "continuationToken": continuationToken ], context: context) } @@ -142,19 +142,22 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { var urlRequest = URLRequest(url: url) urlRequest.httpMethod = method + var headers: [String: String] = [:] + if let json = json { - urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + headers["Content-Type"] = "application/json" urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: json) } else if let form = form { - urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + headers["Content-Type"] = "application/x-www-form-urlencoded" urlRequest.httpBody = Self.encodeForm(form).data(using: .utf8) } - urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") - urlRequest.setValue(context.correlationId().uuidString, forHTTPHeaderField: "client-request-id") + headers["Accept"] = "application/json" + headers["client-request-id"] = context.correlationId().uuidString let request = MSIDHttpRequest() request.urlRequest = urlRequest + request.headers = headers request.context = context request.responseSerializer = MSALNativeAuthV2HALResponseSerializer() request.errorHandler = MSALNativeAuthV2ResponseErrorHandler() From e90c15f99f7371385226d9d90919c81f8700e205 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 30 Jun 2026 16:52:36 +0100 Subject: [PATCH 03/85] other flows - sign-in/sign-up, parsing urls properly --- .../v2/MSALNativeAuthV2FlowController.swift | 397 +++++++++++++++--- .../v2/MSALNativeAuthV2FlowResult.swift | 28 +- .../v2/MSALNativeAuthHALResponse.swift | 17 +- ...SALNativeAuthV2HALResponseSerializer.swift | 35 +- .../MSALNativeAuthV2ResponseValidator.swift | 47 ++- .../MSALNativeAuthV2ValidatedResponses.swift | 25 ++ .../network/v2/MSALNativeAuthV2Endpoint.swift | 4 + .../v2/MSALNativeAuthV2RequestProvider.swift | 73 +++- .../MSALNativeAuthV2FlowControllerTests.swift | 135 +++++- .../MSALNativeAuthV2RequestProviderMock.swift | 44 ++ ...ALNativeAuthV2ResponseValidatorTests.swift | 6 + 11 files changed, 732 insertions(+), 79 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 437734ee14..222e4ae1cc 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -98,43 +98,139 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { - return notImplemented(apiId: .telemetryApiIdSignUp, correlationId: parameters.correlationId, flow: "Sign up V2") + let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUp, context: context) + + // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token + sign_up link). + let bootstrap = await performAuthorizeChallengeStart(context: context) + guard case .continuationToken(let bootstrapToken, let links) = bootstrap else { + return failure(bootstrap, event: event, context: context) + } + + // Step 2 — sign-up start (auto-triggers the email challenge). + let startResult = await performInteraction(context: context) { + try self.requestProvider.signUpStart( + username: parameters.username, + continuationToken: bootstrapToken, + href: links["sign_up"] ?? links["signup"], + context: context + ) + } + + return await mapInteraction(startResult, flowType: .signUp, username: parameters.username, event: event, context: context) } func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { - return notImplemented(apiId: .telemetryApiIdSignInWithCodeStart, correlationId: parameters.correlationId, flow: "Sign in V2") + let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) + let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil ? .telemetryApiIdSignInWithPasswordStart : .telemetryApiIdSignInWithCodeStart + let event = makeAndStartTelemetryEvent(id: apiId, context: context) + + // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token + sign_in link). + let bootstrap = await performAuthorizeChallengeStart(context: context) + guard case .continuationToken(let bootstrapToken, let links) = bootstrap else { + return failure(bootstrap, event: event, context: context) + } + + // Step 2 — sign-in (method discovery). + let startResult = await performInteraction(context: context) { + try self.requestProvider.signInStart( + username: parameters.username, + continuationToken: bootstrapToken, + href: links["sign_in"] ?? links["signin"], + context: context + ) + } + + guard case .signInMethods(let token2, let methods) = startResult else { + return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, event: event, context: context) + } + + // Step 3 — pick a method: password when a password was supplied, otherwise the first OTP method. + let passwordMethod = methods.first { ($0.type ?? "") == "password" } + let otpMethod = methods.first { ($0.type ?? "") != "password" } + let chosen: MSALNativeAuthHALResponse.EmbeddedMethod? + if parameters.password != nil, let passwordMethod = passwordMethod { + chosen = passwordMethod + } else { + chosen = otpMethod ?? passwordMethod + } + + guard let method = chosen, let challengeHref = method.links["challenge"] else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "No usable sign-in method returned")), event: event, context: context) + } + + // Step 4 — challenge the chosen method. + let challengeResult = await performInteraction(context: context) { + try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) + } + + // Step 5 — when a password was supplied and the password factor is required, submit it now + // so password sign-in completes in one call (mirroring V1). + if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult, let verifyHref = verifyHref { + let verifyResult = await performInteraction(context: context) { + try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) + } + return await mapInteraction(verifyResult, flowType: .signIn, username: parameters.username, event: event, context: context) + } + + return await mapInteraction(challengeResult, flowType: .signIn, username: parameters.username, event: event, context: context) } // MARK: - Continuation +} + +extension MSALNativeAuthV2FlowController { func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) let continuation = state.continuation guard let verifyHref = continuation.link("verify")?.absoluteString else { + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), event: event, context: context) } - let result = await performInteraction(context: context) { - try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) - } - - switch result { - case .updateRequired(let token, let updateHref): - let newState = makeState(.resetPassword, continuationToken: token, links: ["update": updateHref], username: continuation.username) - stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) - case .error(let error): - // Recoverable: allow the app to retry with the same code-required state. - return interactionFailure(result, event: event, context: context, newState: error.isInvalidCode ? state : nil) - default: - return interactionFailure(result, event: event, context: context, newState: nil) + // Sign in / sign up use `code`; reset password uses `otp`. + switch continuation.flowType { + case .signIn, .signUp: + let apiId: MSALNativeAuthTelemetryApiId = continuation.flowType == .signUp ? .telemetryApiIdSignUpSubmitCode : .telemetryApiIdSignInSubmitCode + let event = makeAndStartTelemetryEvent(id: apiId, context: context) + let result = await performInteraction(context: context) { + try self.requestProvider.submitCode(href: verifyHref, code: code, continuationToken: continuation.continuationToken, context: context) + } + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) + case .resetPassword: + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) + let result = await performInteraction(context: context) { + try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) + } + switch result { + case .updateRequired(let token, let updateHref): + let newState = makeState(.resetPassword, continuationToken: token, links: ["update": updateHref], username: continuation.username) + stopTelemetryEvent(event, context: context) + return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) + case .error(let error): + // Recoverable: allow the app to retry with the same code-required state. + return interactionFailure(result, event: event, context: context, newState: error.isInvalidCode ? state : nil) + default: + return interactionFailure(result, event: event, context: context, newState: nil) + } } } func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { - return notImplemented(apiId: .telemetryApiIdSignInSubmitPassword, correlationId: nil, flow: "Submit password V2") + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignInSubmitPassword, context: context) + let continuation = state.continuation + + guard let verifyHref = continuation.link("verify")?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), event: event, context: context) + } + + let result = await performInteraction(context: context) { + try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: continuation.continuationToken, context: context) + } + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) } func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -191,35 +287,22 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Password reset did not complete in time")), event: event, context: context) } - // Step 7 — completion authorize-challenge → authorization code. - let codeResult = await performAuthorizeChallengeContinue(continuationToken: completionToken, context: context) - guard case .authorizationCode(let code) = codeResult else { - return failure(codeResult, event: event, context: context) - } + return await completeWithToken(continuationToken: completionToken, username: continuation.username, event: event, context: context) + } - // Step 8 — token exchange. - let tokenRequestResult: Result = await send { - try self.requestProvider.token(code: code, context: context) - } - let tokenResult = responseValidator.validateToken(tokenRequestResult) + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUpSubmitAttributes, context: context) + let continuation = state.continuation - switch tokenResult { - case .success: - guard let accountResult = makeUserAccountResult(username: continuation.username, context: context) else { - let error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to construct account result") - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) - } - stopTelemetryEvent(event, context: context) - return response(.completed(accountResult), context: context) - case .error(let error): - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) + guard let submitHref = continuation.link("submitAttributes")?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing submit-attributes link")), event: event, context: context) } - } - func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { - return notImplemented(apiId: .telemetryApiIdSignUpSubmitAttributes, correlationId: nil, flow: "Submit attributes V2") + let result = await performInteraction(context: context) { + try self.requestProvider.submitAttributes(href: submitHref, attributes: attributes, continuationToken: continuation.continuationToken, context: context) + } + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) } func selectAuthMethod( @@ -227,11 +310,63 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa verificationContact: String?, state: MSALNativeAuthFlowState ) async -> MSALNativeAuthV2FlowControllerResponse { - return notImplemented(apiId: .telemetryApiIdMFAGetAuthMethods, correlationId: nil, flow: "Select auth method V2") + let context = MSALNativeAuthRequestContext(correlationId: nil) + let continuation = state.continuation + + // JIT (strong-auth registration) carries an `enroll` link; MFA carries a `challenge` link. + if continuation.link("enroll") != nil, + let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link("enroll"))?.absoluteString { + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdJITChallenge, context: context) + let result = await performInteraction(context: context) { + try self.requestProvider.registerMethod(href: enrollHref, target: verificationContact, continuationToken: continuation.continuationToken, context: context) + } + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context) + } + + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdMFAGetAuthMethods, context: context) + guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link("challenge"))?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link for selected method")), event: event, context: context) + } + + let result = await performInteraction(context: context) { + try self.requestProvider.challenge(href: challengeHref, continuationToken: continuation.continuationToken, context: context) + } + + // An MFA method challenge surfaces as a verification-required action. + switch result { + case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): + let newState = makeState( + continuation.flowType, + continuationToken: token, + links: ["verify": verifyHref, "resend": resendHref], + username: continuation.username, + sentToHint: sentTo.isEmpty ? continuation.sentToHint : sentTo, + codeLength: codeLength + ) + stopTelemetryEvent(event, context: context) + return response(.actionRequired( + action: .mfaVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + newState: newState + ), context: context) + default: + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context) + } } func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { - return notImplemented(apiId: .telemetryApiIdMFASubmitChallenge, correlationId: nil, flow: "Submit challenge V2") + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdMFASubmitChallenge, context: context) + let continuation = state.continuation + + // JIT activation uses the `activate` link; MFA uses the `verify` link. + guard let submitHref = (continuation.link("activate") ?? continuation.link("verify"))?.absoluteString else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify/activate link")), event: event, context: context) + } + + let result = await performInteraction(context: context) { + try self.requestProvider.submitCode(href: submitHref, code: challenge, continuationToken: continuation.continuationToken, context: context) + } + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) } func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -293,6 +428,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } // MARK: - Result mapping +} + +extension MSALNativeAuthV2FlowController { private func handleCodeRequired( _ result: MSALNativeAuthV2InteractionValidatedResponse, @@ -325,13 +463,136 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } + /// Maps a validated interaction response onto a controller response (the unified, server-driven + /// branch used by sign in / sign up / MFA / JIT continuation steps). On a terminal `continue` + /// state it runs the completion (authorize-challenge → token) sequence. + private func mapInteraction( + _ result: MSALNativeAuthV2InteractionValidatedResponse, + flowType: MSALNativeAuthV2FlowType, + username: String?, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext, + recoverableState: MSALNativeAuthFlowState? = nil + ) async -> MSALNativeAuthV2FlowControllerResponse { + switch result { + case .readyToComplete(let token): + return await completeWithToken(continuationToken: token, username: username, event: event, context: context) + case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): + let newState = makeState( + flowType, + continuationToken: token, + links: ["verify": verifyHref, "resend": resendHref], + username: username, + sentToHint: sentTo.isEmpty ? nil : sentTo, + codeLength: codeLength + ) + stopTelemetryEvent(event, context: context) + return response(.actionRequired( + action: .codeRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + newState: newState + ), context: context) + case .passwordRequired(let token, let verifyHref): + let newState = makeState(flowType, continuationToken: token, links: ["verify": verifyHref], username: username) + stopTelemetryEvent(event, context: context) + return response(.actionRequired(action: .passwordRequired, newState: newState), context: context) + case .updateRequired(let token, let updateHref): + let newState = makeState(flowType, continuationToken: token, links: ["update": updateHref], username: username) + stopTelemetryEvent(event, context: context) + return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) + case .attributesRequired(let token, let attributes, let submitHref): + let newState = makeState(flowType, continuationToken: token, links: ["submitAttributes": submitHref], username: username) + stopTelemetryEvent(event, context: context) + return response(.actionRequired( + action: .attributesRequired(attributes: requiredAttributes(from: attributes)), + newState: newState + ), context: context) + case .mfaRequired(let token, let methods, let challengeHref): + let (authMethods, methodLinks) = authMethods(from: methods) + let newState = makeState( + flowType, + continuationToken: token, + links: ["challenge": challengeHref], + username: username, + authMethods: authMethods, + methodLinks: methodLinks + ) + stopTelemetryEvent(event, context: context) + return response(.actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), context: context) + case .registrationRequired(let token, let enrollHref, let methods): + let (authMethods, methodLinks) = authMethods(from: methods) + let newState = makeState( + flowType, + continuationToken: token, + links: ["enroll": enrollHref], + username: username, + authMethods: authMethods, + methodLinks: methodLinks + ) + stopTelemetryEvent(event, context: context) + return response(.actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), context: context) + case .activationRequired(let token, let activateHref, let sentTo, let codeLength): + let newState = makeState( + flowType, + continuationToken: token, + links: ["activate": activateHref], + username: username, + sentToHint: sentTo.isEmpty ? nil : sentTo, + codeLength: codeLength + ) + stopTelemetryEvent(event, context: context) + return response(.actionRequired( + action: .strongAuthVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + newState: newState + ), context: context) + case .error(let error): + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: error.isInvalidCode || error.kind == .invalidPassword ? recoverableState : nil), context: context) + default: + return interactionFailure(result, event: event, context: context, newState: nil) + } + } + + /// Completion sequence shared by every flow: authorize-challenge (continue) → token exchange. + private func completeWithToken( + continuationToken: String, + username: String?, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext + ) async -> MSALNativeAuthV2FlowControllerResponse { + let codeResult = await performAuthorizeChallengeContinue(continuationToken: continuationToken, context: context) + guard case .authorizationCode(let code) = codeResult else { + return failure(codeResult, event: event, context: context) + } + + let tokenRequestResult: Result = await send { + try self.requestProvider.token(code: code, context: context) + } + let tokenResult = responseValidator.validateToken(tokenRequestResult) + + switch tokenResult { + case .success: + guard let accountResult = makeUserAccountResult(username: username, context: context) else { + let error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to construct account result") + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + stopTelemetryEvent(event, context: context) + return response(.completed(accountResult), context: context) + case .error(let error): + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + } + private func makeState( _ flowType: MSALNativeAuthV2FlowType, continuationToken: String, links: [String: String?], username: String?, sentToHint: String? = nil, - codeLength: Int? = nil + codeLength: Int? = nil, + authMethods: [MSALAuthMethod] = [], + methodLinks: [String: String] = [:] ) -> MSALNativeAuthFlowState { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) var resolvedLinks: [String: URL] = [:] @@ -340,17 +601,59 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa resolvedLinks[relation] = url } } + for (methodId, href) in methodLinks { + if let url = try? resolver.url(forHref: href) { + resolvedLinks["method:\(methodId)"] = url + } + } let continuation = MSALNativeAuthV2ContinuationState( flowType: flowType, continuationToken: continuationToken, links: resolvedLinks, username: username, sentToHint: sentToHint, - codeLength: codeLength + codeLength: codeLength, + authMethods: authMethods ) return MSALNativeAuthFlowState(continuation: continuation, controller: self) } + /// Converts embedded HAL methods into public ``MSALAuthMethod`` objects plus a map of each + /// method's `challenge` href (keyed by method id) for later selection. + private func authMethods( + from methods: [MSALNativeAuthHALResponse.EmbeddedMethod] + ) -> (methods: [MSALAuthMethod], methodLinks: [String: String]) { + var out: [MSALAuthMethod] = [] + var methodLinks: [String: String] = [:] + for method in methods { + let id = method.id ?? "" + let type = method.type ?? "" + out.append(MSALAuthMethod( + id: id, + challengeType: type, + channelTargetType: MSALNativeAuthChannelType(value: type), + loginHint: method.hint + )) + if let challenge = method.links["challenge"] { + methodLinks[id] = challenge + } + } + return (out, methodLinks) + } + + private func requiredAttributes( + from attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry] + ) -> [MSALNativeAuthRequiredAttribute] { + return attributes.map { entry in + MSALNativeAuthRequiredAttribute( + name: entry.id ?? "", + type: entry.type ?? "", + required: entry.required, + regex: entry.regex + ) + } + } + private func makeUserAccountResult(username: String?, context: MSALNativeAuthRequestContext) -> MSALNativeAuthUserAccountResult? { let environment = config.authority.url.host ?? "login.microsoftonline.com" let homeAccountId = MSALAccountId(accountIdentifier: "", objectId: "", tenantId: "") diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift index 23d34ea795..2ccd3a9bb4 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -38,15 +38,41 @@ enum MSALNativeAuthV2FlowType { struct MSALNativeAuthV2ContinuationState { let flowType: MSALNativeAuthV2FlowType let continuationToken: String - /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue"). + /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", + /// "challenge", "enroll", "activate", "submitAttributes"). Per-method links are keyed "method:". let links: [String: URL] let username: String? let sentToHint: String? let codeLength: Int? + /// Auth methods offered for MFA / strong-auth (JIT) selection. + let authMethods: [MSALAuthMethod] + + init( + flowType: MSALNativeAuthV2FlowType, + continuationToken: String, + links: [String: URL], + username: String?, + sentToHint: String? = nil, + codeLength: Int? = nil, + authMethods: [MSALAuthMethod] = [] + ) { + self.flowType = flowType + self.continuationToken = continuationToken + self.links = links + self.username = username + self.sentToHint = sentToHint + self.codeLength = codeLength + self.authMethods = authMethods + } func link(_ relation: String) -> URL? { return links[relation] } + + /// The challenge / enroll link associated with a specific auth method. + func methodLink(for methodId: String) -> URL? { + return links["method:\(methodId)"] + } } /// Result produced by the unified V2 controller for a single step of a flow. diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 64eeddc0ab..059802b617 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -36,7 +36,7 @@ import Foundation struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { /// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). - struct EmbeddedMethod { + struct EmbeddedMethod: Equatable { let id: String? let type: String? let hint: String? @@ -44,6 +44,14 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let links: [String: String] } + /// An attribute the server requires during sign up (`collectAttributes` action). + struct RequiredAttributeEntry: Equatable { + let id: String? + let type: String? + let required: Bool + let regex: String? + } + /// A server error body (`{ "error": { ... } }`). struct ServerError { let code: String? @@ -61,6 +69,13 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let codeLength: Int? let hint: String? + /// Top-level method identifier (`id`) on method-style responses (sign up `start`, JIT `activate`). + let methodId: String? + /// Top-level method type (`type`, e.g. "email") on method-style responses. + let methodType: String? + /// Attributes the server requests on a sign up `collectAttributes` response. + let attributes: [RequiredAttributeEntry] + /// Authorization code from the final `authorize-challenge` call (step 7). let code: String? /// Access token from the `/token` exchange (step 8). diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 0c0fad9aff..91a1e07f06 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -48,6 +48,9 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali continuationToken: nil, codeLength: nil, hint: nil, + methodId: nil, + methodType: nil, + attributes: [], code: nil, accessToken: nil, links: [:], @@ -71,21 +74,35 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali continuationToken: resource.string(forKey: "continuationToken") ?? resource.string(forKey: "continuation_token"), codeLength: json["codeLength"] as? Int, hint: resource.string(forKey: "hint"), + methodId: resource.string(forKey: "id"), + methodType: resource.string(forKey: "type"), + attributes: Self.parseAttributes(from: json), code: resource.string(forKey: "code"), accessToken: resource.string(forKey: "access_token"), - links: Self.parseLinks(from: resource), + links: Self.parseLinks(from: resource, json: json), methods: Self.parseMethods(from: resource), error: Self.parseError(from: json, fallbackCorrelationId: correlationId) ) } - private static func parseLinks(from resource: MSIDHALResource) -> [String: String] { + /// Known link relations the server may return as flat, top-level string fields rather than + /// nested under HAL `_links` (e.g. the `authorize-challenge` bootstrap returns + /// `reset_password` / `sign_in` / `sign_up` at the top level). + private static let topLevelLinkRelations = ["reset_password", "sign_in", "sign_up", "signin", "signup"] + + private static func parseLinks(from resource: MSIDHALResource, json: [String: Any]) -> [String: String] { var result: [String: String] = [:] for (relation, links) in resource.links { if let href = links.first?.href { result[relation] = href } } + // Merge flat, top-level link relations (HAL `_links` takes precedence if both are present). + for relation in topLevelLinkRelations where result[relation] == nil { + if let href = json[relation] as? String { + result[relation] = href + } + } return result } @@ -108,6 +125,20 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali } } + private static func parseAttributes(from json: [String: Any]) -> [MSALNativeAuthHALResponse.RequiredAttributeEntry] { + guard let rawAttributes = json["attributes"] as? [[String: Any]] else { + return [] + } + return rawAttributes.map { dict in + MSALNativeAuthHALResponse.RequiredAttributeEntry( + id: (dict["attributeId"] as? String) ?? (dict["id"] as? String), + type: dict["type"] as? String, + required: (dict["required"] as? Bool) ?? false, + regex: (dict["validationRegex"] as? String) ?? (dict["regex"] as? String) + ) + } + } + private static func parseError(from json: [String: Any], fallbackCorrelationId: UUID?) -> MSALNativeAuthHALResponse.ServerError? { guard let errorDict = json["error"] as? [String: Any] else { return nil diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 62839a6f27..8d2d4ef306 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -75,22 +75,59 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing continuation token in interaction response")) } + // Sign-in method discovery: no action, but the available methods are embedded. + if response.action == nil, !response.methods.isEmpty { + return .signInMethods(continuationToken: continuationToken, methods: response.methods) + } + switch response.action { case "challenge": let method = response.methods.first return .challengeRequired( continuationToken: continuationToken, - challengeHref: method?.links["challenge"], - hint: method?.hint + challengeHref: method?.links["challenge"] ?? response.href(forRelation: "challenge"), + hint: method?.hint ?? response.hint ) case "verify": - return .codeRequired( + // After a password, a `challenge` link plus embedded methods means MFA is required. + if let challengeHref = response.href(forRelation: "challenge"), !response.methods.isEmpty { + return .mfaRequired( + continuationToken: continuationToken, + methods: response.methods, + challengeHref: challengeHref + ) + } + let verifyHref = response.href(forRelation: "verify") + // An email/OOB method carries a hint and/or a code length; a password method does not. + if (response.codeLength ?? 0) > 0 || response.hint != nil || response.methodType == "email" { + return .codeRequired( + continuationToken: continuationToken, + verifyHref: verifyHref, + resendHref: response.href(forRelation: "resend"), + sentTo: response.hint ?? "", + codeLength: response.codeLength ?? 0 + ) + } + return .passwordRequired(continuationToken: continuationToken, verifyHref: verifyHref) + case "enroll", "register": + return .registrationRequired( continuationToken: continuationToken, - verifyHref: response.href(forRelation: "verify"), - resendHref: response.href(forRelation: "resend"), + enrollHref: response.href(forRelation: "enroll") ?? response.href(forRelation: "register"), + methods: response.methods + ) + case "activate": + return .activationRequired( + continuationToken: continuationToken, + activateHref: response.href(forRelation: "activate"), sentTo: response.hint ?? "", codeLength: response.codeLength ?? 0 ) + case "collectAttributes": + return .attributesRequired( + continuationToken: continuationToken, + attributes: response.attributes, + submitHref: response.href(forRelation: "submitAttributes") ?? response.href(forRelation: "submitattributes") + ) case "update": return .updateRequired( continuationToken: continuationToken, diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index 9b69dc854a..373c026d33 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -51,10 +51,23 @@ enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { /// A single enum represents every HAL interaction response; the validator selects the case /// from the HAL `state` / `action` pair. enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { + /// Sign-in method discovery: the `signin` response carries a continuation token and the + /// available authentication methods (each with its own `challenge` link). + case signInMethods(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) /// `action == challenge`: a verification method is available; the SDK should auto-trigger the challenge. case challengeRequired(continuationToken: String, challengeHref: String?, hint: String?) + /// `action == verify` on a password method: the user must enter their password. + case passwordRequired(continuationToken: String, verifyHref: String?) /// `action == verify`: a one-time code is required from the user. case codeRequired(continuationToken: String, verifyHref: String?, resendHref: String?, sentTo: String, codeLength: Int) + /// `action == verify` after a password, carrying a `challenge` link and the MFA methods. + case mfaRequired(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod], challengeHref: String?) + /// `action == enroll`/`register`: strong-auth (JIT) registration is required; pick a method to enroll. + case registrationRequired(continuationToken: String, enrollHref: String?, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) + /// `action == activate`: a JIT enrollment code is required from the user. + case activationRequired(continuationToken: String, activateHref: String?, sentTo: String, codeLength: Int) + /// `action == collectAttributes`: sign-up attributes are required from the user. + case attributesRequired(continuationToken: String, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry], submitHref: String?) /// `action == update`: a new password is required from the user. case updateRequired(continuationToken: String, updateHref: String?) /// `action == poll`: the operation is still running; keep polling. @@ -65,10 +78,22 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { + case let (.signInMethods(lToken, lMethods), .signInMethods(rToken, rMethods)): + return lToken == rToken && lMethods == rMethods case let (.challengeRequired(lToken, lHref, lHint), .challengeRequired(rToken, rHref, rHint)): return lToken == rToken && lHref == rHref && lHint == rHint + case let (.passwordRequired(lToken, lHref), .passwordRequired(rToken, rHref)): + return lToken == rToken && lHref == rHref case let (.codeRequired(lToken, lVerify, lResend, lSent, lLen), .codeRequired(rToken, rVerify, rResend, rSent, rLen)): return lToken == rToken && lVerify == rVerify && lResend == rResend && lSent == rSent && lLen == rLen + case let (.mfaRequired(lToken, lMethods, lHref), .mfaRequired(rToken, rMethods, rHref)): + return lToken == rToken && lMethods == rMethods && lHref == rHref + case let (.registrationRequired(lToken, lHref, lMethods), .registrationRequired(rToken, rHref, rMethods)): + return lToken == rToken && lHref == rHref && lMethods == rMethods + case let (.activationRequired(lToken, lHref, lSent, lLen), .activationRequired(rToken, rHref, rSent, rLen)): + return lToken == rToken && lHref == rHref && lSent == rSent && lLen == rLen + case let (.attributesRequired(lToken, lAttrs, lHref), .attributesRequired(rToken, rAttrs, rHref)): + return lToken == rToken && lAttrs == rAttrs && lHref == rHref case let (.updateRequired(lToken, lHref), .updateRequired(rToken, rHref)): return lToken == rToken && lHref == rHref case let (.pollInProgress(lToken, lHref), .pollInProgress(rToken, rHref)): diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift index 61578cc961..526d600903 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift @@ -36,4 +36,8 @@ enum MSALNativeAuthV2Endpoint: String, CaseIterable { case token = "/oauth2/v2.0/token" /// Self-service password reset entry (step 2). case resetPasswordStart = "/api/v0.1/auth/resetpassword" + /// Sign in entry (used when the bootstrap response omits a `sign_in` link). + case signInStart = "/api/v0.1/signin/start" + /// Sign up entry (used when the bootstrap response omits a `sign_up` link). + case signUpStart = "/api/v0.1/signup/start" } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 0cb9927560..489739decb 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -41,6 +41,24 @@ protocol MSALNativeAuthV2RequestProviding { /// Step 2: SSPR entry (fixed endpoint). func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Sign in entry: posts the username to the bootstrap `sign_in` href (or the fixed endpoint). + func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Sign up entry: posts the username to the bootstrap `sign_up` href (or the fixed endpoint). + func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Submit a password to a server `verify` href (sign in / MFA primary factor). + func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Submit a one-time `code` to a server `verify` / `activate` href (sign in / sign up / MFA / JIT). + func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Submit collected attributes (sign up) to a server `submitAttributes` href. + func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Register a strong-auth method (JIT) by posting the target to a server `enroll` href. + func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Step 3: send EOTP (server `challenge` / `resend` href). func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest @@ -67,9 +85,12 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(for: .authorizeChallenge) + // The bootstrap authorize-challenge sends ONLY `client_id`. Including `scope` here makes + // ESTS mint a continuation token scoped for the authorization-code path, which the + // signup/signin/resetpassword `start` endpoints reject with AADSTS55200 ("continuation_token + // is invalid"). `scope` is supplied later on the `token` call instead. return makeRequest(url: url, method: "POST", form: [ - "client_id": config.clientId, - "scope": defaultScope + "client_id": config.clientId ], context: context) } @@ -100,6 +121,54 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { ], context: context) } + func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try href.map { try resolver.url(forHref: $0) } ?? resolver.url(for: .signInStart) + return makeRequest(url: url, method: "POST", json: [ + "username": username, + "continuationToken": continuationToken + ], context: context) + } + + func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try href.map { try resolver.url(forHref: $0) } ?? resolver.url(for: .signUpStart) + return makeRequest(url: url, method: "POST", json: [ + "username": username, + "continuationToken": continuationToken + ], context: context) + } + + func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + return makeRequest(url: url, method: "POST", json: [ + "password": password, + "continuationToken": continuationToken + ], context: context) + } + + func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + return makeRequest(url: url, method: "POST", json: [ + "code": code, + "continuationToken": continuationToken + ], context: context) + } + + func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + var body = attributes + body["continuationToken"] = continuationToken + return makeRequest(url: url, method: "POST", json: body, context: context) + } + + func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + let url = try resolver.url(forHref: href) + var body: [String: Any] = ["continuationToken": continuationToken] + if let target = target { + body["target"] = target + } + return makeRequest(url: url, method: "POST", json: body, context: context) + } + func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(forHref: href) return makeRequest(url: url, method: "POST", json: [ diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index 0fbb666970..91447de7a6 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -226,40 +226,133 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { XCTAssertTrue(requestProviderMock.challengeCalled) } - // MARK: - notImplemented flows + // MARK: - signUp / signIn / MFA / JIT + + private func makeState( + flowType: MSALNativeAuthV2FlowType, + links: [String: URL], + authMethods: [MSALAuthMethod] = [], + continuationToken: String = "ct" + ) -> MSALNativeAuthFlowState { + let continuation = MSALNativeAuthV2ContinuationState( + flowType: flowType, + continuationToken: continuationToken, + links: links, + username: "user@contoso.com", + sentToHint: "u***@contoso.com", + codeLength: 8, + authMethods: authMethods + ) + return MSALNativeAuthFlowState(continuation: continuation, controller: sut) + } + + func test_signUp_happyPath_returnsCodeRequired() async { + requestProviderMock.mockRequest() + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-bootstrap", links: ["sign_up": "https://contoso.com/signup"]) + ] + validatorMock.interactionResponses = [ + .codeRequired(continuationToken: "ct-2", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + ] - func test_signUp_returnsNotImplemented() async { let response = await sut.signUp(parameters: MSALNativeAuthSignUpParameters(username: "user@contoso.com")) - assertNotImplemented(response) + + guard case .actionRequired(let action, _) = response.result, case .codeRequired = action else { + return XCTFail("Expected codeRequired action, got \(response.result)") + } + XCTAssertTrue(requestProviderMock.signUpStartCalled) } - func test_signIn_returnsNotImplemented() async { + func test_signIn_withPassword_happyPath_returnsCompleted() async { + requestProviderMock.mockRequest() + let passwordMethod = MSALNativeAuthHALResponse.EmbeddedMethod( + id: "1", type: "password", hint: nil, links: ["challenge": "https://contoso.com/pw/challenge"]) + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-bootstrap", links: ["sign_in": "https://contoso.com/signin"]), + .authorizationCode(code: "auth-code") + ] + validatorMock.interactionResponses = [ + .signInMethods(continuationToken: "ct-2", methods: [passwordMethod]), + .passwordRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/pw/verify"), + .readyToComplete(continuationToken: "ct-4") + ] + validatorMock.tokenResponse = .success(accessToken: "access-token") + + let params = MSALNativeAuthSignInParameters(username: "user@contoso.com") + params.password = "password" + let response = await sut.signIn(parameters: params) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") + } + XCTAssertTrue(requestProviderMock.signInStartCalled) + XCTAssertTrue(requestProviderMock.submitPasswordCalled) + XCTAssertTrue(requestProviderMock.tokenCalled) + } + + func test_signIn_withCode_returnsCodeRequired() async { + requestProviderMock.mockRequest() + let emailMethod = MSALNativeAuthHALResponse.EmbeddedMethod( + id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/email/challenge"]) + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-bootstrap", links: ["sign_in": "https://contoso.com/signin"]) + ] + validatorMock.interactionResponses = [ + .signInMethods(continuationToken: "ct-2", methods: [emailMethod]), + .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + ] + let response = await sut.signIn(parameters: MSALNativeAuthSignInParameters(username: "user@contoso.com")) - assertNotImplemented(response) + + guard case .actionRequired(let action, _) = response.result, case .codeRequired = action else { + return XCTFail("Expected codeRequired action, got \(response.result)") + } } - func test_submitPassword_returnsNotImplemented() async { - let state = makeState(links: [:]) + func test_submitPassword_whenMFARequired_returnsMFARequired() async { + requestProviderMock.mockRequest() + let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: [:]) + validatorMock.interactionResponses = [ + .mfaRequired(continuationToken: "ct-mfa", methods: [method], challengeHref: "https://contoso.com/mfa/challenge") + ] + let state = makeState(flowType: .signIn, links: ["verify": URL(string: "https://contoso.com/pw/verify")!]) + let response = await sut.submitPassword("password", state: state) - assertNotImplemented(response) - } - func test_submitAttributes_returnsNotImplemented() async { - let state = makeState(links: [:]) - let response = await sut.submitAttributes([:], state: state) - assertNotImplemented(response) + guard case .actionRequired(let action, _) = response.result, case .mfaRequired = action else { + return XCTFail("Expected mfaRequired action, got \(response.result)") + } + XCTAssertTrue(requestProviderMock.submitPasswordCalled) } - func test_submitChallenge_returnsNotImplemented() async { - let state = makeState(links: [:]) - let response = await sut.submitChallenge("challenge", state: state) - assertNotImplemented(response) + func test_submitAttributes_happyPath_returnsCompleted() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] + validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] + validatorMock.tokenResponse = .success(accessToken: "access-token") + let state = makeState(flowType: .signUp, links: ["submitAttributes": URL(string: "https://contoso.com/submitattributes")!]) + + let response = await sut.submitAttributes(["displayName": "User"], state: state) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") + } + XCTAssertTrue(requestProviderMock.submitAttributesCalled) } - private func assertNotImplemented(_ response: MSALNativeAuthV2FlowControllerResponse) { - guard case .error(let error, _) = response.result else { - return XCTFail("Expected error, got \(response.result)") + func test_submitChallenge_happyPath_returnsCompleted() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] + validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] + validatorMock.tokenResponse = .success(accessToken: "access-token") + let state = makeState(flowType: .signIn, links: ["verify": URL(string: "https://contoso.com/mfa/verify")!]) + + let response = await sut.submitChallenge("12345678", state: state) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") } - XCTAssertTrue(error.isNotImplemented) + XCTAssertTrue(requestProviderMock.submitCodeCalled) + XCTAssertTrue(requestProviderMock.tokenCalled) } } diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index 53163825c6..fce615fb3a 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -34,6 +34,12 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { private(set) var authorizeChallengeContinueCalled = false private(set) var tokenCalled = false private(set) var resetPasswordStartCalled = false + private(set) var signInStartCalled = false + private(set) var signUpStartCalled = false + private(set) var submitPasswordCalled = false + private(set) var submitCodeCalled = false + private(set) var submitAttributesCalled = false + private(set) var registerMethodCalled = false private(set) var challengeCalled = false private(set) var verifyCalled = false private(set) var updatePasswordCalled = false @@ -41,6 +47,10 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { private(set) var challengeHrefReceived: String? private(set) var verifyHrefReceived: String? + private(set) var submitPasswordHrefReceived: String? + private(set) var submitCodeHrefReceived: String? + private(set) var submitAttributesHrefReceived: String? + private(set) var registerMethodHrefReceived: String? private(set) var updateHrefReceived: String? private(set) var pollHrefReceived: String? @@ -77,6 +87,40 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { return try resolveRequest() } + func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + signInStartCalled = true + return try resolveRequest() + } + + func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + signUpStartCalled = true + return try resolveRequest() + } + + func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + submitPasswordCalled = true + submitPasswordHrefReceived = href + return try resolveRequest() + } + + func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + submitCodeCalled = true + submitCodeHrefReceived = href + return try resolveRequest() + } + + func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + submitAttributesCalled = true + submitAttributesHrefReceived = href + return try resolveRequest() + } + + func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + registerMethodCalled = true + registerMethodHrefReceived = href + return try resolveRequest() + } + func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { challengeCalled = true challengeHrefReceived = href diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index f5960b4814..b73312d4e4 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -43,6 +43,9 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { continuationToken: String? = nil, codeLength: Int? = nil, hint: String? = nil, + methodId: String? = nil, + methodType: String? = nil, + attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry] = [], code: String? = nil, accessToken: String? = nil, links: [String: String] = [:], @@ -57,6 +60,9 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { continuationToken: continuationToken, codeLength: codeLength, hint: hint, + methodId: methodId, + methodType: methodType, + attributes: attributes, code: code, accessToken: accessToken, links: links, From c72464bf78b4609ca09e99dd03b39c33f6ce7e4a Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 30 Jun 2026 17:55:02 +0100 Subject: [PATCH 04/85] url resolver --- .../v2/MSALNativeAuthV2HrefURLResolver.swift | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 3a4e08edd2..7f844371e3 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -56,6 +56,14 @@ struct MSALNativeAuthV2HrefURLResolver { } /// Resolves a server-provided `_links` href into an absolute URL against the authority host. + /// + /// The server returns hrefs whose leading path segment is a tenant identifier — typically the + /// tenant **GUID** (e.g. `/4710d5e4-.../api/v0.1/signup/start`). However, the bootstrap + /// continuation_token is bound to the tenant form used by the authority + /// (`.onmicrosoft.com`); calling the GUID path makes ESTS reject the token with + /// AADSTS55200 ("continuation_token is invalid"). To keep the tenant identifier consistent for + /// the whole flow, the leading tenant segment is dropped and the remaining API path is grafted + /// onto the authority's path, reproducing the URL against the configured authority. func url(forHref href: String) throws -> URL { let trimmed = href.trimmingCharacters(in: .whitespacesAndNewlines) @@ -69,8 +77,7 @@ struct MSALNativeAuthV2HrefURLResolver { // Relative / templated href: the href may already carry its own query string // (e.g. `?dc=...`), so parse it with URLComponents to separate path from query - // rather than folding the query into the path. Strip any leading tenant - // placeholder and resolve the path against the authority's scheme + host. + // rather than folding the query into the path. guard let hrefComponents = URLComponents(string: normalizedHref(from: trimmed)) else { throw MSALNativeAuthInternalError.invalidUrl } @@ -78,11 +85,38 @@ struct MSALNativeAuthV2HrefURLResolver { guard var components = URLComponents(url: authorityURL, resolvingAgainstBaseURL: true) else { throw MSALNativeAuthInternalError.invalidUrl } - components.path = hrefComponents.path + + // Drop the href's leading tenant segment and reproduce the path against the authority's + // tenant path so the tenant identifier stays consistent with the bootstrap. + components.path = authorityTenantPath + apiPath(from: hrefComponents.path) components.percentEncodedQuery = hrefComponents.percentEncodedQuery return try applyingDataCenter(to: components) } + /// The authority's path (its tenant segment), without a trailing slash. + private var authorityTenantPath: String { + let path = authorityURL.path + if path.hasSuffix("/") { + return String(path.dropLast()) + } + return path + } + + /// Returns the API portion of a server href path, dropping any leading tenant segment. + /// + /// Native Auth V2 hrefs are of the form `//api/v0.1/...` (the tenant being a GUID or + /// `.onmicrosoft.com`). We key off the first known API marker and keep the path from + /// there, so the tenant segment is removed regardless of its form. Hrefs that are already + /// host-relative (no tenant prefix) are returned unchanged (with a guaranteed leading slash). + private func apiPath(from path: String) -> String { + for marker in ["/api/", "/oauth2/"] { + if let range = path.range(of: marker) { + return String(path[range.lowerBound...]) + } + } + return path.hasPrefix("/") ? path : "/" + path + } + private func normalizedHref(from href: String) -> String { var result = href From 365df84aa1f50e8cfa27bfed68c3e69da2ac3bb7 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 1 Jul 2026 15:44:14 +0100 Subject: [PATCH 05/85] Updates for error handling and proper signin response --- .../v2/MSALNativeAuthV2FlowController.swift | 46 +++++++++++++------ .../MSALNativeAuthV2ResponseValidator.swift | 30 ++++++++++++ .../v2/MSALNativeAuthV2RequestProvider.swift | 10 ++-- .../v2/MSALNativeAuthFlowError.swift | 6 +++ 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 222e4ae1cc..ad77b0755d 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -141,22 +141,38 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - guard case .signInMethods(let token2, let methods) = startResult else { - return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, event: event, context: context) - } - - // Step 3 — pick a method: password when a password was supplied, otherwise the first OTP method. - let passwordMethod = methods.first { ($0.type ?? "") == "password" } - let otpMethod = methods.first { ($0.type ?? "") != "password" } - let chosen: MSALNativeAuthHALResponse.EmbeddedMethod? - if parameters.password != nil, let passwordMethod = passwordMethod { - chosen = passwordMethod - } else { - chosen = otpMethod ?? passwordMethod - } + // Step 3 — resolve the token and the method challenge href. + // The server may either return `.signInMethods` (action == nil, methods embedded) so the + // client picks a method, or collapse discovery and return `.challengeRequired` (action == + // "challenge") directly with the chosen method's challenge href already resolved. + let token2: String + let challengeHref: String + + switch startResult { + case .signInMethods(let token, let methods): + // Pick a method: password when a password was supplied, otherwise the first OTP method. + let passwordMethod = methods.first { ($0.type ?? "") == "password" } + let otpMethod = methods.first { ($0.type ?? "") != "password" } + let chosen: MSALNativeAuthHALResponse.EmbeddedMethod? + if parameters.password != nil, let passwordMethod = passwordMethod { + chosen = passwordMethod + } else { + chosen = otpMethod ?? passwordMethod + } - guard let method = chosen, let challengeHref = method.links["challenge"] else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "No usable sign-in method returned")), event: event, context: context) + guard let method = chosen, let href = method.links["challenge"] else { + return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "No usable sign-in method returned")), event: event, context: context) + } + token2 = token + challengeHref = href + case .challengeRequired(let token, let href, _): + guard let href = href else { + return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, event: event, context: context) + } + token2 = token + challengeHref = href + default: + return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, event: event, context: context) } // Step 4 — challenge the chosen method. diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 8d2d4ef306..409f79fb6b 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -162,6 +162,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin private static func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { let message = serverError.message + let errorCodes = Self.estsErrorCodes(from: message) let kind: MSALNativeAuthFlowError.Kind if serverError.innerErrorCode == "invalidContinuationToken" { @@ -170,6 +171,11 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin kind = serverError.code == "invalidGrant" ? .invalidCode : .invalidContinuationToken } else if let message = message, message.contains("AADSTS50034") { kind = .userNotFound + } else if serverError.innerErrorCode == "invalidUserNameOrPassword" + || errorCodes.contains(MSALNativeAuthESTSApiErrorCodes.invalidCredentials.rawValue) { + // Wrong username/password at sign in (AADSTS50126). Mirrors the V1 `.invalidCredentials` + // handling: a recoverable credentials error, not an invalid one-time code. + kind = .invalidPassword } else if serverError.code == "invalidGrant" { kind = .invalidCode } else { @@ -179,10 +185,34 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return MSALNativeAuthFlowError( kind: kind, errorDescription: message, + errorCodes: errorCodes, correlationId: serverError.correlationId ) } + /// Extracts the numeric ESTS error codes (e.g. `50126` from `AADSTS50126`) embedded in a + /// server error message, mirroring the `error_codes` array the V1 flows surface. + private static func estsErrorCodes(from message: String?) -> [Int] { + guard let message = message else { + return [] + } + var codes: [Int] = [] + let scanner = Scanner(string: message) + let marker = "AADSTS" + while !scanner.isAtEnd { + guard scanner.scanUpToString(marker) != nil || scanner.string.hasPrefix(marker) else { + break + } + guard scanner.scanString(marker) != nil else { + break + } + if let code = scanner.scanInt() { + codes.append(code) + } + } + return codes + } + private static func flowError(from error: Error) -> MSALNativeAuthFlowError { if let flowError = error as? MSALNativeAuthFlowError { return flowError diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 489739decb..972f36f686 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -76,7 +76,6 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { private let config: MSALNativeAuthInternalConfiguration private let resolver: MSALNativeAuthV2HrefURLResolver - private let defaultScope = "openid offline_access profile" init(config: MSALNativeAuthInternalConfiguration) { self.config = config @@ -96,20 +95,21 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(for: .authorizeChallenge) + // The authorize-challenge-resume call sends ONLY `continuation_token` (no client_id/scope), + // matching the server contract; extra parameters cause AADSTS55200 / auth failures. return makeRequest(url: url, method: "POST", form: [ - "client_id": config.clientId, - "scope": defaultScope, "continuation_token": continuationToken ], context: context) } func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(for: .token) + // The authorization_code exchange sends only client_id, code and grant_type. `scope` was + // already established during the authorize-challenge bootstrap and must not be resent here. return makeRequest(url: url, method: "POST", form: [ "grant_type": "authorization_code", "code": code, - "client_id": config.clientId, - "scope": defaultScope + "client_id": config.clientId ], context: context) } diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift index 781201f45f..2384354d84 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift @@ -86,6 +86,12 @@ public class MSALNativeAuthFlowError: NSObject, LocalizedError { return kind == .invalidCode } + /// Whether the submitted password was rejected (wrong credentials at sign in, or a password + /// that did not satisfy the server's policy during sign up). + public var isInvalidPassword: Bool { + return kind == .invalidPassword + } + /// Whether the username was not found in the directory. public var isUserNotFound: Bool { return kind == .userNotFound From f5cb672ccc5a4064e6ec29cc32c32cdb9d2bd8c7 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 1 Jul 2026 15:56:10 +0100 Subject: [PATCH 06/85] parse token --- .../v2/MSALNativeAuthV2FlowController.swift | 134 +++++++++++++----- .../v2/MSALNativeAuthV2RequestProvider.swift | 14 +- 2 files changed, 110 insertions(+), 38 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index ad77b0755d..9c3a6aa5a8 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -38,6 +38,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa private let requestProvider: MSALNativeAuthV2RequestProviding private let responseValidator: MSALNativeAuthV2ResponseValidating private let cacheAccessor: MSALNativeAuthCacheInterface + private let resultFactory: MSALNativeAuthResultBuildable private let maxPollAttempts = 5 private let pollIntervalNanoseconds: UInt64 = 1_500_000_000 // 1.5s @@ -46,12 +47,14 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa config: MSALNativeAuthInternalConfiguration, requestProvider: MSALNativeAuthV2RequestProviding, responseValidator: MSALNativeAuthV2ResponseValidating, - cacheAccessor: MSALNativeAuthCacheInterface + cacheAccessor: MSALNativeAuthCacheInterface, + resultFactory: MSALNativeAuthResultBuildable ) { self.config = config self.requestProvider = requestProvider self.responseValidator = responseValidator self.cacheAccessor = cacheAccessor + self.resultFactory = resultFactory super.init(clientId: config.clientId) } @@ -60,7 +63,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa config: config, requestProvider: MSALNativeAuthV2RequestProvider(config: config), responseValidator: MSALNativeAuthV2ResponseValidator(), - cacheAccessor: cacheAccessor + cacheAccessor: cacheAccessor, + resultFactory: MSALNativeAuthResultFactory(config: config, cacheAccessor: cacheAccessor) ) } @@ -569,6 +573,9 @@ extension MSALNativeAuthV2FlowController { } /// Completion sequence shared by every flow: authorize-challenge (continue) → token exchange. + /// The `/token` response is persisted to the shared MSAL token cache — exactly like the V1 + /// sign-in flow — so the returned ``MSALNativeAuthUserAccountResult`` can vend access tokens + /// via `getAccessToken(...)`. private func completeWithToken( continuationToken: String, username: String?, @@ -580,26 +587,102 @@ extension MSALNativeAuthV2FlowController { return failure(codeResult, event: event, context: context) } - let tokenRequestResult: Result = await send { - try self.requestProvider.token(code: code, context: context) + let tokenResponseResult = await performTokenExchange(code: code, context: context) + switch tokenResponseResult { + case .success(let tokenResponse): + do { + let msidConfiguration = resultFactory.makeMSIDConfiguration(scopes: Self.scopes(from: tokenResponse)) + let tokenResult = try cacheTokenResponse(tokenResponse, context: context, msidConfiguration: msidConfiguration) + + guard let accountResult = resultFactory.makeUserAccountResult(tokenResult: tokenResult, context: context) else { + let error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to construct account result") + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + stopTelemetryEvent(event, context: context) + return response(.completed(accountResult), context: context) + } catch { + let flowError = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to save tokens to the cache") + stopTelemetryEvent(event, context: context, error: flowError) + return response(.error(error: flowError, newState: nil), context: context) + } + case .failure(let error): + let flowError = (error as? MSALNativeAuthFlowError) ?? MSALNativeAuthFlowError(kind: .generalError, errorDescription: (error as NSError).localizedDescription) + stopTelemetryEvent(event, context: context, error: flowError) + return response(.error(error: flowError, newState: nil), context: context) } - let tokenResult = responseValidator.validateToken(tokenRequestResult) + } - switch tokenResult { - case .success: - guard let accountResult = makeUserAccountResult(username: username, context: context) else { - let error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to construct account result") - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) + /// Sends the `/token` request and parses the raw OAuth JSON into an `MSIDTokenResponse`. + private func performTokenExchange( + code: String, + context: MSALNativeAuthRequestContext + ) async -> Result { + let request: MSIDHttpRequest + do { + request = try requestProvider.token(code: code, context: context) + } catch { + return .failure(error) + } + + return await withCheckedContinuation { continuation in + request.send { response, error in + if let error = error { + continuation.resume(returning: .failure(error)) + return + } + guard let responseDict = response as? [AnyHashable: Any] else { + continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) + return + } + do { + let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) + tokenResponse.correlationId = tokenResponse.correlationId ?? request.context?.correlationId().uuidString + continuation.resume(returning: .success(tokenResponse)) + } catch { + continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) + } } - stopTelemetryEvent(event, context: context) - return response(.completed(accountResult), context: context) - case .error(let error): - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) } } + /// Persists the token response (tokens + account) to the shared MSAL cache and returns the + /// resulting `MSIDTokenResult`. Mirrors the V1 `cacheTokenResponse` implementation. + private func cacheTokenResponse( + _ tokenResponse: MSIDTokenResponse, + context: MSALNativeAuthRequestContext, + msidConfiguration: MSIDConfiguration + ) throws -> MSIDTokenResult { + // Remove any existing account for this configuration before saving the new tokens. + if let accounts = try? cacheAccessor.getAllAccounts(configuration: msidConfiguration), + let account = accounts.first, + let identifier = MSIDAccountIdentifier(displayableId: account.username, homeAccountId: account.identifier) { + try? cacheAccessor.clearCache( + accountIdentifier: identifier, + authority: msidConfiguration.authority, + clientId: msidConfiguration.clientId, + context: context + ) + } + + guard let tokenResult = try cacheAccessor.validateAndSaveTokensAndAccount( + tokenResponse: tokenResponse, + configuration: msidConfiguration, + context: context + ) else { + throw MSALNativeAuthInternalError.invalidResponse + } + return tokenResult + } + + /// Extracts the granted scopes from a token response so the cache target matches what was issued. + private static func scopes(from tokenResponse: MSIDTokenResponse) -> [String] { + guard let scope = tokenResponse.scope, !scope.isEmpty else { + return [] + } + return scope.components(separatedBy: " ").filter { !$0.isEmpty } + } + private func makeState( _ flowType: MSALNativeAuthV2FlowType, continuationToken: String, @@ -670,25 +753,6 @@ extension MSALNativeAuthV2FlowController { } } - private func makeUserAccountResult(username: String?, context: MSALNativeAuthRequestContext) -> MSALNativeAuthUserAccountResult? { - let environment = config.authority.url.host ?? "login.microsoftonline.com" - let homeAccountId = MSALAccountId(accountIdentifier: "", objectId: "", tenantId: "") - guard let account = MSALAccount( - username: username ?? "", - homeAccountId: homeAccountId, - environment: environment, - tenantProfiles: [] - ) else { - return nil - } - return MSALNativeAuthUserAccountResult( - account: account, - rawIdToken: nil, - configuration: config, - cacheAccessor: cacheAccessor - ) - } - // MARK: - Response construction private func response( diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 972f36f686..9391b5cc9a 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -106,11 +106,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { let url = try resolver.url(for: .token) // The authorization_code exchange sends only client_id, code and grant_type. `scope` was // already established during the authorize-challenge bootstrap and must not be resent here. + // The `/token` endpoint returns a standard OAuth token response (NOT HAL), so this request + // skips the HAL serializer and yields the raw JSON dictionary for the controller to parse + // into an `MSIDTokenResponse` and persist to the cache (mirroring the V1 sign-in flow). return makeRequest(url: url, method: "POST", form: [ "grant_type": "authorization_code", "code": code, "client_id": config.clientId - ], context: context) + ], context: context, rawJSONResponse: true) } func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { @@ -206,7 +209,8 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { method: String, json: [String: Any]? = nil, form: [String: String]? = nil, - context: MSALNativeAuthRequestContext + context: MSALNativeAuthRequestContext, + rawJSONResponse: Bool = false ) -> MSIDHttpRequest { var urlRequest = URLRequest(url: url) urlRequest.httpMethod = method @@ -228,7 +232,11 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { request.urlRequest = urlRequest request.headers = headers request.context = context - request.responseSerializer = MSALNativeAuthV2HALResponseSerializer() + // The `/token` endpoint returns a plain OAuth response rather than HAL; leaving the response + // serializer unset makes `MSIDHttpRequest.send` hand back the raw JSON dictionary. + if !rawJSONResponse { + request.responseSerializer = MSALNativeAuthV2HALResponseSerializer() + } request.errorHandler = MSALNativeAuthV2ResponseErrorHandler() if let interceptor = config.requestInterceptor { From 90bcb7f9aa7f09d283e2f1f4486f71b4d7f35dcc Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 2 Jul 2026 15:09:10 +0100 Subject: [PATCH 07/85] Fix sending attributes --- .../network/v2/MSALNativeAuthV2RequestProvider.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 9391b5cc9a..aee36561a1 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -158,8 +158,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { let url = try resolver.url(forHref: href) - var body = attributes - body["continuationToken"] = continuationToken + let body: [String: Any] = [ + "attributes": attributes, + "continuationToken": continuationToken + ] return makeRequest(url: url, method: "POST", json: body, context: context) } From 6a9af7447d73ba28ed672f48ad1cec6b63793a81 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 6 Jul 2026 16:49:51 +0100 Subject: [PATCH 08/85] Move HAL to MSAL --- MSAL/IdentityCore | 2 +- MSAL/MSAL.xcodeproj/project.pbxproj | 6 + MSAL/module.modulemap | 2 - .../network/responses/v2/HALResource.swift | 146 ++++++++++++++++++ .../v2/MSALNativeAuthHALResponse.swift | 2 +- ...SALNativeAuthV2HALResponseSerializer.swift | 12 +- 6 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 MSAL/src/native_auth/network/responses/v2/HALResource.swift diff --git a/MSAL/IdentityCore b/MSAL/IdentityCore index 0d6ae66c7f..16c26a474f 160000 --- a/MSAL/IdentityCore +++ b/MSAL/IdentityCore @@ -1 +1 @@ -Subproject commit 0d6ae66c7f2ba594a2d3c824545c8cb9dc8860bf +Subproject commit 16c26a474ff5e98dbdc618caf834b187d56e2370 diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 47fde98430..37d9774559 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -417,6 +417,7 @@ 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + FADE0000000000000000AA03 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -994,6 +995,7 @@ BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; D61A64941E5AA7D60086D120 /* MSALTestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64801E5AA7C60086D120 /* MSALTestAppDelegate.m */; }; D61A64951E5AA7D60086D120 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64811E5AA7C60086D120 /* main.m */; }; @@ -2316,6 +2318,7 @@ 963377BE211E14C600943EE0 /* MSALWebviewType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWebviewType.m; sourceTree = ""; }; 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALResponse.swift; sourceTree = ""; }; + FADE0000000000000000AA01 /* HALResource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HALResource.swift; sourceTree = ""; }; 9648AF54225D826500F66801 /* MSALTelemetryConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTelemetryConfig+Internal.h"; sourceTree = ""; }; 9648AF5B225DD6A900F66801 /* MSALGlobalConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALGlobalConfig+Internal.h"; sourceTree = ""; }; 9682A62A218290F700E37E63 /* MSALDefinitions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDefinitions.h; sourceTree = ""; }; @@ -3537,6 +3540,7 @@ isa = PBXGroup; children = ( 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */, + FADE0000000000000000AA01 /* HALResource.swift */, C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */, 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */, 1A153D161101EAF09A32E906 /* validator */, @@ -7319,6 +7323,7 @@ 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */, 8E0486CA55F25C1987E4067A /* MSALNativeAuthV2FlowController.swift in Sources */, CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */, + FADE0000000000000000AA02 /* HALResource.swift in Sources */, 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, @@ -7622,6 +7627,7 @@ E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */, 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */, 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */, + FADE0000000000000000AA03 /* HALResource.swift in Sources */, 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, diff --git a/MSAL/module.modulemap b/MSAL/module.modulemap index 10ee87773b..dd2fe8668c 100644 --- a/MSAL/module.modulemap +++ b/MSAL/module.modulemap @@ -69,8 +69,6 @@ module MSAL_Private { header "IdentityCore/IdentityCore/src/validation/MSIDAuthority+Internal.h" header "IdentityCore/IdentityCore/src/network/response_serializer/MSIDResponseSerialization.h" header "IdentityCore/IdentityCore/src/network/response_serializer/MSIDAADTokenResponseSerializer.h" - header "IdentityCore/IdentityCore/src/network/response_serializer/hal/MSIDHALResource.h" - header "IdentityCore/IdentityCore/src/network/response_serializer/hal/MSIDHALLink.h" header "IdentityCore/IdentityCore/src/requests/sdk/MSIDTokenResponseValidator.h" header "IdentityCore/IdentityCore/src/MSIDError.h" header "IdentityCore/IdentityCore/src/MSIDTelemetryStringSerializable.h" diff --git a/MSAL/src/native_auth/network/responses/v2/HALResource.swift b/MSAL/src/native_auth/network/responses/v2/HALResource.swift new file mode 100644 index 0000000000..e08a48a9d7 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/HALResource.swift @@ -0,0 +1,146 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a HAL Link Object as defined by the JSON-HAL specification. +/// +/// See: https://www.ietf.org/archive/id/draft-kelly-json-hal-11.html +internal struct HALLink +{ + /// The URI of the linked resource. + let href: String + + /// Secondary key distinguishing links within the same relation. + let name: String? + + /// Whether `href` is a URI Template (RFC 6570). + let templated: Bool + + init(href: String, name: String? = nil, templated: Bool = false) + { + self.href = href + self.name = name + self.templated = templated + } + + /// Parses a HAL Link Object from a JSON dictionary. + init?(json: [String: Any]) + { + guard let href = json["href"] as? String else { return nil } + self.href = href + self.name = json["name"] as? String + self.templated = json["templated"] as? Bool ?? false + } +} + +/// Generic parser for HAL+JSON documents. +/// +/// Handles extraction of `_links` and `_embedded` sections, +/// and provides typed accessors for common HAL patterns. +internal struct HALResource +{ + /// The raw JSON properties (excluding `_links` and `_embedded`). + let properties: [String: Any] + + /// All links keyed by relation type. + let links: [String: [HALLink]] + + /// All embedded resources keyed by relation type. + let embedded: [String: [[String: Any]]] + + /// Parses a HAL resource from a JSON dictionary. + init(json: [String: Any]) + { + var props = json + var parsedLinks: [String: [HALLink]] = [:] + var parsedEmbedded: [String: [[String: Any]]] = [:] + + // Parse _links + if let linksJson = json["_links"] as? [String: Any] + { + for (rel, value) in linksJson + { + if rel == "curies" { continue } + + if let linkDict = value as? [String: Any], let link = HALLink(json: linkDict) + { + parsedLinks[rel] = [link] + } + else if let linkArray = value as? [[String: Any]] + { + parsedLinks[rel] = linkArray.compactMap { HALLink(json: $0) } + } + } + props.removeValue(forKey: "_links") + } + + // Parse _embedded + if let embeddedJson = json["_embedded"] as? [String: Any] + { + for (rel, value) in embeddedJson + { + if let array = value as? [[String: Any]] + { + parsedEmbedded[rel] = array + } + else if let single = value as? [String: Any] + { + parsedEmbedded[rel] = [single] + } + } + props.removeValue(forKey: "_embedded") + } + + self.properties = props + self.links = parsedLinks + self.embedded = parsedEmbedded + } + + // MARK: - Accessors + + /// Returns a single link for the given relation, or nil if not present. + func link(rel: String) -> HALLink? + { + return links[rel]?.first + } + + /// Returns all links for the given relation. + func allLinks(rel: String) -> [HALLink] + { + return links[rel] ?? [] + } + + /// Returns embedded resources for the given relation. + func embeddedResources(rel: String) -> [[String: Any]] + { + return embedded[rel] ?? [] + } + + /// Returns a string property value. + func string(forKey key: String) -> String? + { + return properties[key] as? String + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 059802b617..60201617d8 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -32,7 +32,7 @@ import Foundation /// and `4xx` error bodies) is parsed into a single ``MSALNativeAuthHALResponse``. The /// V2 response validator then inspects `error`, `state` and `action` to decide how the /// flow should proceed. HAL parsing itself is delegated to the shared -/// `MSIDHALResource` / `MSIDHALLink` types in IdentityCore. +/// `HALResource` / `HALLink` Swift types. struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { /// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 91a1e07f06..c80f732011 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -31,7 +31,7 @@ import Foundation /// Unlike the V1 serializer (Decodable + snake_case), V2 responses are HAL+JSON and /// every HTTP outcome carries a meaningful body, so this serializer never throws on a /// non-200 status — it captures the status code and lets the V2 validator decide. HAL -/// `_links` / `_embedded` extraction is delegated to the shared `MSIDHALResource`. +/// `_links` / `_embedded` extraction is delegated to the shared `HALResource`. final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSerialization { func responseObject(for httpResponse: HTTPURLResponse?, data: Data?, context: MSIDRequestContext?) throws -> Any { @@ -64,7 +64,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali throw MSALNativeAuthInternalError.responseSerializationError(headerCorrelationId: correlationId) } - let resource = MSIDHALResource(json: json) + let resource = HALResource(json: json) return MSALNativeAuthHALResponse( statusCode: statusCode, @@ -90,7 +90,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali /// `reset_password` / `sign_in` / `sign_up` at the top level). private static let topLevelLinkRelations = ["reset_password", "sign_in", "sign_up", "signin", "signup"] - private static func parseLinks(from resource: MSIDHALResource, json: [String: Any]) -> [String: String] { + private static func parseLinks(from resource: HALResource, json: [String: Any]) -> [String: String] { var result: [String: String] = [:] for (relation, links) in resource.links { if let href = links.first?.href { @@ -106,10 +106,10 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali return result } - private static func parseMethods(from resource: MSIDHALResource) -> [MSALNativeAuthHALResponse.EmbeddedMethod] { - let methodResources = resource.embeddedResources(forRelation: "methods") + private static func parseMethods(from resource: HALResource) -> [MSALNativeAuthHALResponse.EmbeddedMethod] { + let methodResources = resource.embeddedResources(rel: "methods") return methodResources.map { dict in - let methodResource = MSIDHALResource(json: dict) + let methodResource = HALResource(json: dict) var links: [String: String] = [:] for (relation, halLinks) in methodResource.links { if let href = halLinks.first?.href { From 2e74940e6966a31b21bb20dc035e0411559e20e7 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 6 Jul 2026 19:07:17 +0100 Subject: [PATCH 09/85] v2 client_info and parameters and scopes --- MSAL/MSAL.xcodeproj/project.pbxproj | 6 + .../v2/MSALNativeAuthV2FlowController.swift | 86 ++++-- .../v2/MSALNativeAuthV2FlowResult.swift | 7 +- .../MSALNativeAuthV2RequestConfigurator.swift | 262 +++++++++++++++++ .../v2/MSALNativeAuthV2RequestProvider.swift | 265 ++++++++---------- ...SALNativeAuthResetPasswordParameters.swift | 5 + .../MSALNativeAuthSignUpParameters.swift | 5 + .../MSALNativeAuthOperationTypes.swift | 17 ++ .../MSALNativeAuthTelemetryApiId.swift | 7 + .../MSALNativeAuthV2FlowControllerTests.swift | 5 +- .../MSALNativeAuthV2RequestProviderMock.swift | 4 +- 11 files changed, 487 insertions(+), 182 deletions(-) create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 37d9774559..45991eafe6 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -92,6 +92,7 @@ 12E2160B2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 12E2160C2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; + D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; @@ -446,6 +447,7 @@ 7248CF9E2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */; }; 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; + D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; 886F515829CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -2378,6 +2380,7 @@ A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAction.swift; sourceTree = ""; }; B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProvider.swift; sourceTree = ""; }; + D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestConfigurator.swift; sourceTree = ""; }; B203459221AF77FB00B221AA /* MSALRedirectUri.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALRedirectUri.h; sourceTree = ""; }; B203459321AF77FB00B221AA /* MSALRedirectUri.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALRedirectUri.m; sourceTree = ""; }; B203459C21AFA1FB00B221AA /* MSALRedirectUri+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALRedirectUri+Internal.h"; sourceTree = ""; }; @@ -3118,6 +3121,7 @@ 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */, F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */, B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */, + D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */, ); name = v2; path = v2; @@ -7331,6 +7335,7 @@ 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */, 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, + D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7635,6 +7640,7 @@ 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */, 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, + D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 9c3a6aa5a8..aede240656 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -73,6 +73,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) + let scopes = joinScopes(parameters.scopes) // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token). let bootstrap = await performAuthorizeChallengeStart(context: context) @@ -98,12 +99,13 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) } - return handleCodeRequired(challengeResult, username: parameters.username, fallbackHint: hint, event: event, context: context) + return handleCodeRequired(challengeResult, username: parameters.username, fallbackHint: hint, scopes: scopes, event: event, context: context) } func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUp, context: context) + let scopes = joinScopes(parameters.scopes) // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token + sign_up link). let bootstrap = await performAuthorizeChallengeStart(context: context) @@ -121,13 +123,14 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - return await mapInteraction(startResult, flowType: .signUp, username: parameters.username, event: event, context: context) + return await mapInteraction(startResult, flowType: .signUp, username: parameters.username, scopes: scopes, event: event, context: context) } func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil ? .telemetryApiIdSignInWithPasswordStart : .telemetryApiIdSignInWithCodeStart let event = makeAndStartTelemetryEvent(id: apiId, context: context) + let scopes = joinScopes(parameters.scopes) // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token + sign_in link). let bootstrap = await performAuthorizeChallengeStart(context: context) @@ -171,12 +174,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa challengeHref = href case .challengeRequired(let token, let href, _): guard let href = href else { - return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, event: event, context: context) + return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) } token2 = token challengeHref = href default: - return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, event: event, context: context) + return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) } // Step 4 — challenge the chosen method. @@ -190,10 +193,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let verifyResult = await performInteraction(context: context) { try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) } - return await mapInteraction(verifyResult, flowType: .signIn, username: parameters.username, event: event, context: context) + return await mapInteraction(verifyResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) } - return await mapInteraction(challengeResult, flowType: .signIn, username: parameters.username, event: event, context: context) + return await mapInteraction(challengeResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) } // MARK: - Continuation @@ -218,7 +221,7 @@ extension MSALNativeAuthV2FlowController { let result = await performInteraction(context: context) { try self.requestProvider.submitCode(href: verifyHref, code: code, continuationToken: continuation.continuationToken, context: context) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) case .resetPassword: let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) let result = await performInteraction(context: context) { @@ -226,7 +229,7 @@ extension MSALNativeAuthV2FlowController { } switch result { case .updateRequired(let token, let updateHref): - let newState = makeState(.resetPassword, continuationToken: token, links: ["update": updateHref], username: continuation.username) + let newState = makeState(.resetPassword, continuationToken: token, links: ["update": updateHref], username: continuation.username, scopes: continuation.scopes) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) case .error(let error): @@ -250,7 +253,7 @@ extension MSALNativeAuthV2FlowController { let result = await performInteraction(context: context) { try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: continuation.continuationToken, context: context) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) } func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -307,7 +310,7 @@ extension MSALNativeAuthV2FlowController { return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Password reset did not complete in time")), event: event, context: context) } - return await completeWithToken(continuationToken: completionToken, username: continuation.username, event: event, context: context) + return await completeWithToken(continuationToken: completionToken, username: continuation.username, scopes: continuation.scopes, event: event, context: context) } func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -322,7 +325,7 @@ extension MSALNativeAuthV2FlowController { let result = await performInteraction(context: context) { try self.requestProvider.submitAttributes(href: submitHref, attributes: attributes, continuationToken: continuation.continuationToken, context: context) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) } func selectAuthMethod( @@ -340,7 +343,7 @@ extension MSALNativeAuthV2FlowController { let result = await performInteraction(context: context) { try self.requestProvider.registerMethod(href: enrollHref, target: verificationContact, continuationToken: continuation.continuationToken, context: context) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context) + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context) } let event = makeAndStartTelemetryEvent(id: .telemetryApiIdMFAGetAuthMethods, context: context) @@ -361,7 +364,8 @@ extension MSALNativeAuthV2FlowController { links: ["verify": verifyHref, "resend": resendHref], username: continuation.username, sentToHint: sentTo.isEmpty ? continuation.sentToHint : sentTo, - codeLength: codeLength + codeLength: codeLength, + scopes: continuation.scopes ) stopTelemetryEvent(event, context: context) return response(.actionRequired( @@ -369,7 +373,7 @@ extension MSALNativeAuthV2FlowController { newState: newState ), context: context) default: - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context) + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context) } } @@ -386,7 +390,7 @@ extension MSALNativeAuthV2FlowController { let result = await performInteraction(context: context) { try self.requestProvider.submitCode(href: submitHref, code: challenge, continuationToken: continuation.continuationToken, context: context) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, event: event, context: context, recoverableState: state) + return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) } func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -402,7 +406,7 @@ extension MSALNativeAuthV2FlowController { try self.requestProvider.challenge(href: resendHref, continuationToken: continuation.continuationToken, context: context) } - return handleCodeRequired(result, username: continuation.username, fallbackHint: continuation.sentToHint, event: event, context: context) + return handleCodeRequired(result, username: continuation.username, fallbackHint: continuation.sentToHint, scopes: continuation.scopes, event: event, context: context) } // MARK: - Shared step helpers @@ -456,6 +460,7 @@ extension MSALNativeAuthV2FlowController { _ result: MSALNativeAuthV2InteractionValidatedResponse, username: String?, fallbackHint: String?, + scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext ) -> MSALNativeAuthV2FlowControllerResponse { @@ -467,7 +472,8 @@ extension MSALNativeAuthV2FlowController { links: ["verify": verifyHref, "resend": resendHref], username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, - codeLength: codeLength + codeLength: codeLength, + scopes: scopes ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo stopTelemetryEvent(event, context: context) @@ -490,13 +496,14 @@ extension MSALNativeAuthV2FlowController { _ result: MSALNativeAuthV2InteractionValidatedResponse, flowType: MSALNativeAuthV2FlowType, username: String?, + scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, recoverableState: MSALNativeAuthFlowState? = nil ) async -> MSALNativeAuthV2FlowControllerResponse { switch result { case .readyToComplete(let token): - return await completeWithToken(continuationToken: token, username: username, event: event, context: context) + return await completeWithToken(continuationToken: token, username: username, scopes: scopes, event: event, context: context) case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( flowType, @@ -504,7 +511,8 @@ extension MSALNativeAuthV2FlowController { links: ["verify": verifyHref, "resend": resendHref], username: username, sentToHint: sentTo.isEmpty ? nil : sentTo, - codeLength: codeLength + codeLength: codeLength, + scopes: scopes ) stopTelemetryEvent(event, context: context) return response(.actionRequired( @@ -512,15 +520,15 @@ extension MSALNativeAuthV2FlowController { newState: newState ), context: context) case .passwordRequired(let token, let verifyHref): - let newState = makeState(flowType, continuationToken: token, links: ["verify": verifyHref], username: username) + let newState = makeState(flowType, continuationToken: token, links: ["verify": verifyHref], username: username, scopes: scopes) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .passwordRequired, newState: newState), context: context) case .updateRequired(let token, let updateHref): - let newState = makeState(flowType, continuationToken: token, links: ["update": updateHref], username: username) + let newState = makeState(flowType, continuationToken: token, links: ["update": updateHref], username: username, scopes: scopes) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) case .attributesRequired(let token, let attributes, let submitHref): - let newState = makeState(flowType, continuationToken: token, links: ["submitAttributes": submitHref], username: username) + let newState = makeState(flowType, continuationToken: token, links: ["submitAttributes": submitHref], username: username, scopes: scopes) stopTelemetryEvent(event, context: context) return response(.actionRequired( action: .attributesRequired(attributes: requiredAttributes(from: attributes)), @@ -534,7 +542,8 @@ extension MSALNativeAuthV2FlowController { links: ["challenge": challengeHref], username: username, authMethods: authMethods, - methodLinks: methodLinks + methodLinks: methodLinks, + scopes: scopes ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), context: context) @@ -546,7 +555,8 @@ extension MSALNativeAuthV2FlowController { links: ["enroll": enrollHref], username: username, authMethods: authMethods, - methodLinks: methodLinks + methodLinks: methodLinks, + scopes: scopes ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), context: context) @@ -557,7 +567,8 @@ extension MSALNativeAuthV2FlowController { links: ["activate": activateHref], username: username, sentToHint: sentTo.isEmpty ? nil : sentTo, - codeLength: codeLength + codeLength: codeLength, + scopes: scopes ) stopTelemetryEvent(event, context: context) return response(.actionRequired( @@ -579,6 +590,7 @@ extension MSALNativeAuthV2FlowController { private func completeWithToken( continuationToken: String, username: String?, + scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2FlowControllerResponse { @@ -587,7 +599,7 @@ extension MSALNativeAuthV2FlowController { return failure(codeResult, event: event, context: context) } - let tokenResponseResult = await performTokenExchange(code: code, context: context) + let tokenResponseResult = await performTokenExchange(code: code, scopes: scopes, context: context) switch tokenResponseResult { case .success(let tokenResponse): do { @@ -616,11 +628,12 @@ extension MSALNativeAuthV2FlowController { /// Sends the `/token` request and parses the raw OAuth JSON into an `MSIDTokenResponse`. private func performTokenExchange( code: String, + scopes: [String], context: MSALNativeAuthRequestContext ) async -> Result { let request: MSIDHttpRequest do { - request = try requestProvider.token(code: code, context: context) + request = try requestProvider.token(code: code, scopes: scopes, context: context) } catch { return .failure(error) } @@ -691,7 +704,8 @@ extension MSALNativeAuthV2FlowController { sentToHint: String? = nil, codeLength: Int? = nil, authMethods: [MSALAuthMethod] = [], - methodLinks: [String: String] = [:] + methodLinks: [String: String] = [:], + scopes: [String] = [] ) -> MSALNativeAuthFlowState { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) var resolvedLinks: [String: URL] = [:] @@ -712,11 +726,25 @@ extension MSALNativeAuthV2FlowController { username: username, sentToHint: sentToHint, codeLength: codeLength, - authMethods: authMethods + authMethods: authMethods, + scopes: scopes ) return MSALNativeAuthFlowState(continuation: continuation, controller: self) } + /// Merges the caller-requested scopes with the default OIDC scopes (openid, profile, + /// offline_access), de-duplicating while preserving order. Mirrors the V1 sign-in behaviour so + /// the same access token / cache target results regardless of the flow version. + private func joinScopes(_ scopes: [String]?) -> [String] { + let defaultOIDCScopes = MSALPublicClientApplication.defaultOIDCScopes().array + guard let scopes = scopes else { + return defaultOIDCScopes as? [String] ?? [] + } + let joinedScopes = NSMutableOrderedSet(array: scopes) + joinedScopes.addObjects(from: defaultOIDCScopes) + return joinedScopes.array as? [String] ?? [] + } + /// Converts embedded HAL methods into public ``MSALAuthMethod`` objects plus a map of each /// method's `challenge` href (keyed by method id) for later selection. private func authMethods( diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift index 2ccd3a9bb4..b86eb5a2a5 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -46,6 +46,9 @@ struct MSALNativeAuthV2ContinuationState { let codeLength: Int? /// Auth methods offered for MFA / strong-auth (JIT) selection. let authMethods: [MSALAuthMethod] + /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final + /// `/token` exchange. Threaded through every step so completion mirrors the V1 sign-in flow. + let scopes: [String] init( flowType: MSALNativeAuthV2FlowType, @@ -54,7 +57,8 @@ struct MSALNativeAuthV2ContinuationState { username: String?, sentToHint: String? = nil, codeLength: Int? = nil, - authMethods: [MSALAuthMethod] = [] + authMethods: [MSALAuthMethod] = [], + scopes: [String] = [] ) { self.flowType = flowType self.continuationToken = continuationToken @@ -63,6 +67,7 @@ struct MSALNativeAuthV2ContinuationState { self.sentToHint = sentToHint self.codeLength = codeLength self.authMethods = authMethods + self.scopes = scopes } func link(_ relation: String) -> URL? { diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift new file mode 100644 index 0000000000..e045947a0a --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -0,0 +1,262 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +// MARK: - Request parameters + +/// Describes a single V2 native-auth request. This mirrors the role of V1's `MSALNativeAuthRequestable`: +/// each concrete parameter type knows its target URL, HTTP method, body, body encoding and telemetry +/// identity. `MSALNativeAuthV2RequestConfigurator` turns any of these into a fully-configured +/// `MSIDHttpRequest` that reuses the shared AAD request pipeline (device-id headers, PkeyAuth, +/// correlation, server telemetry) — so V2 requests carry the same headers as V1. +protocol MSALNativeAuthV2Requestable { + var context: MSALNativeAuthRequestContext { get } + var httpMethod: String { get } + var encoding: MSALNativeAuthUrlRequestEncoding { get } + var apiId: MSALNativeAuthTelemetryApiId { get } + var operationType: MSALNativeAuthOperationType { get } + /// `true` only for the `/token` endpoint, which returns a plain OAuth response (not HAL) and must + /// keep the default raw-JSON response serializer instead of the HAL serializer. + var expectsRawJSONResponse: Bool { get } + var body: [AnyHashable: Any] { get } + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL +} + +extension MSALNativeAuthV2Requestable { + var httpMethod: String { "POST" } + var expectsRawJSONResponse: Bool { false } +} + +/// The destination of a V2 request: either a well-known endpoint or a server-provided HAL `href`. +enum MSALNativeAuthV2RequestTarget { + case endpoint(MSALNativeAuthV2Endpoint) + case href(String) + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + switch self { + case .endpoint(let endpoint): + return try resolver.url(for: endpoint) + case .href(let href): + return try resolver.url(forHref: href) + } + } +} + +/// `POST /authorize/challenge` bootstrap. Sends ONLY `client_id` (form encoded). +struct MSALNativeAuthV2AuthorizeChallengeStartParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let clientId: String + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeStart.rawValue + + var body: [AnyHashable: Any] { + return ["client_id": clientId] + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(for: .authorizeChallenge) + } +} + +/// `POST /authorize/challenge` resume. Sends ONLY `continuation_token` (form encoded). +struct MSALNativeAuthV2AuthorizeChallengeContinueParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let continuationToken: String + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeContinue.rawValue + + var body: [AnyHashable: Any] { + return ["continuation_token": continuationToken] + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(for: .authorizeChallenge) + } +} + +/// `POST /token` authorization-code exchange. Form encoded, raw OAuth (non-HAL) response. Includes +/// `client_info=true` so ESTS returns the `client_info` blob required to persist tokens to the cache. +struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let clientId: String + let code: String + let scopes: [String] + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2Token + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.token.rawValue + let expectsRawJSONResponse = true + + var body: [AnyHashable: Any] { + var form: [AnyHashable: Any] = [ + "grant_type": "authorization_code", + "code": code, + "client_id": clientId, + MSALNativeAuthRequestParametersKey.clientInfo.rawValue: true.description + ] + if !scopes.isEmpty { + form["scope"] = scopes.joined(separator: " ") + } + return form + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(for: .token) + } +} + +/// The signup/signin/resetpassword `start` entry requests. JSON encoded `{username, continuationToken}`, +/// targeting either the well-known start endpoint or a server-provided `href`. +struct MSALNativeAuthV2EntryParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let target: MSALNativeAuthV2RequestTarget + let apiId: MSALNativeAuthTelemetryApiId + let operationType: MSALNativeAuthOperationType + let username: String + let continuationToken: String + let encoding: MSALNativeAuthUrlRequestEncoding = .json + + var body: [AnyHashable: Any] { + return ["username": username, "continuationToken": continuationToken] + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try target.url(resolver: resolver) + } +} + +/// A HAL follow-up request driven by a server-provided `href` (challenge, verify, submit*, register, +/// update-password, poll). JSON encoded with a caller-supplied body. +struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let href: String + let httpMethod: String + let apiId: MSALNativeAuthTelemetryApiId + let operationType: MSALNativeAuthOperationType + let body: [AnyHashable: Any] + let encoding: MSALNativeAuthUrlRequestEncoding = .json + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(forHref: href) + } +} + +// MARK: - Request configurator + +/// Builds a fully-configured `MSIDHttpRequest` for any `MSALNativeAuthV2Requestable`, reusing the same +/// shared AAD request pipeline as V1 native auth. Subclassing `MSIDAADRequestConfigurator` gives V2 the +/// standard device-id (`x-client-*`) headers, app metadata, PkeyAuth, `Accept: application/json`, +/// correlation headers and the authority network-host rewrite. On top of that it attaches the V2 HAL / +/// raw-JSON response serializer, the V2 error handler, server telemetry and the request interceptor. +final class MSALNativeAuthV2RequestConfigurator: MSIDAADRequestConfigurator { + + private let config: MSALNativeAuthInternalConfiguration + private let resolver: MSALNativeAuthV2HrefURLResolver + + init(config: MSALNativeAuthInternalConfiguration) { + self.config = config + self.resolver = MSALNativeAuthV2HrefURLResolver(config: config) + } + + func configure(parameters: MSALNativeAuthV2Requestable) throws -> MSIDHttpRequest { + let url = try parameters.url(resolver: resolver) + + let request = MSIDHttpRequest() + // Capture the default raw-JSON response serializer before the base `configure(_:)` swaps it for + // the AAD serializer, so the `/token` endpoint (a plain OAuth response, not HAL) can restore it. + let rawJSONResponseSerializer = request.responseSerializer + + request.context = parameters.context + // `MSIDHttpRequest.parameters` is typed `[String: String]` for AAD form posts, but V2 HAL bodies + // can contain nested JSON (e.g. the sign-up `attributes` object). Assign via KVC so the nested + // dictionary is preserved and JSON-serialized as-is by `MSALNativeAuthUrlRequestSerializer`. + request.setValue(parameters.body, forKey: "parameters") + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = parameters.httpMethod + request.urlRequest = urlRequest + + request.requestSerializer = MSALNativeAuthUrlRequestSerializer( + context: parameters.context, + encoding: parameters.encoding + ) + + // Reuse the shared AAD request pipeline (same base configuration V1 native auth relies on). + configure(request) + + // `MSIDAADRequestConfigurator` writes the standard headers onto `urlRequest`, but the native + // auth request serializer rebuilds `allHTTPHeaderFields` from `request.headers` at send time. + // Copy the configured headers across so the device/PkeyAuth/correlation headers survive + // serialization and reach the wire. (Server-telemetry headers are applied post-serialization by + // `MSIDHttpRequest.send`, so they survive regardless.) + if let configuredHeaders = request.urlRequest?.allHTTPHeaderFields { + request.headers = configuredHeaders + } + + request.serverTelemetry = MSALNativeAuthServerTelemetry( + currentRequestTelemetry: MSALNativeAuthCurrentRequestTelemetry( + apiId: parameters.apiId, + operationType: parameters.operationType, + platformFields: nil + ), + context: parameters.context + ) + + if parameters.expectsRawJSONResponse { + request.responseSerializer = rawJSONResponseSerializer + } else { + request.responseSerializer = MSALNativeAuthV2HALResponseSerializer() + } + request.errorHandler = MSALNativeAuthV2ResponseErrorHandler() + + if let interceptor = config.requestInterceptor { + request.requestInterceptor = MSALNativeAuthV2RequestInterceptorBridge(interceptor: interceptor) + } + + return request + } +} + +/// Bridges `MSALNativeAuthRequestInterceptor` (Swift public protocol) to `MSIDHttpRequestInterceptorProtocol` (ObjC). +private final class MSALNativeAuthV2RequestInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { + + private let interceptor: MSALNativeAuthRequestInterceptor + + init(interceptor: MSALNativeAuthRequestInterceptor) { + self.interceptor = interceptor + } + + func addAdditionalHeaderFields( + for requestUrl: URL?, + with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock + ) { + interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in + completionBlock(additionalHeaders) + } + } +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index aee36561a1..b40b16ef8b 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -36,7 +36,7 @@ protocol MSALNativeAuthV2RequestProviding { func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest /// Step 8: token exchange. - func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest /// Step 2: SSPR entry (fixed endpoint). func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest @@ -75,207 +75,172 @@ protocol MSALNativeAuthV2RequestProviding { final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { private let config: MSALNativeAuthInternalConfiguration - private let resolver: MSALNativeAuthV2HrefURLResolver + private let configurator: MSALNativeAuthV2RequestConfigurator init(config: MSALNativeAuthInternalConfiguration) { self.config = config - self.resolver = MSALNativeAuthV2HrefURLResolver(config: config) + self.configurator = MSALNativeAuthV2RequestConfigurator(config: config) } func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(for: .authorizeChallenge) // The bootstrap authorize-challenge sends ONLY `client_id`. Including `scope` here makes // ESTS mint a continuation token scoped for the authorization-code path, which the // signup/signin/resetpassword `start` endpoints reject with AADSTS55200 ("continuation_token - // is invalid"). `scope` is supplied later on the `token` call instead. - return makeRequest(url: url, method: "POST", form: [ - "client_id": config.clientId - ], context: context) + // is invalid"). `scope` is supplied later on the `token` call instead (see `token(code:scopes:context:)`). + return try configurator.configure(parameters: MSALNativeAuthV2AuthorizeChallengeStartParameters(context: context, clientId: config.clientId)) } func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(for: .authorizeChallenge) // The authorize-challenge-resume call sends ONLY `continuation_token` (no client_id/scope), // matching the server contract; extra parameters cause AADSTS55200 / auth failures. - return makeRequest(url: url, method: "POST", form: [ - "continuation_token": continuationToken - ], context: context) + return try configurator.configure( + parameters: MSALNativeAuthV2AuthorizeChallengeContinueParameters(context: context, continuationToken: continuationToken) + ) } - func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(for: .token) - // The authorization_code exchange sends only client_id, code and grant_type. `scope` was - // already established during the authorize-challenge bootstrap and must not be resent here. + func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + // The authorization_code exchange sends client_id, code, grant_type and the requested + // `scope` (caller scopes merged with the default OIDC scopes), mirroring the V1 sign-in + // token call. `scope` is sent here — not on the authorize-challenge bootstrap — because + // including it in the bootstrap makes ESTS mint a continuation token the start endpoints + // reject with AADSTS55200. // The `/token` endpoint returns a standard OAuth token response (NOT HAL), so this request - // skips the HAL serializer and yields the raw JSON dictionary for the controller to parse - // into an `MSIDTokenResponse` and persist to the cache (mirroring the V1 sign-in flow). - return makeRequest(url: url, method: "POST", form: [ - "grant_type": "authorization_code", - "code": code, - "client_id": config.clientId - ], context: context, rawJSONResponse: true) + // yields the raw JSON dictionary for the controller to parse into an `MSIDTokenResponse` and + // persist to the cache (mirroring the V1 sign-in flow). + // `client_info=true` (added by the parameter class) asks ESTS to return the `client_info` + // blob (uid/utid) in the token response. IdentityCore's AAD-v2/CIAM factory rejects any + // token response without it ("Client info was not returned in the server response"), which + // surfaces to the caller as "Unable to save tokens to the cache". + return try configurator.configure(parameters: MSALNativeAuthV2TokenParameters( + context: context, + clientId: config.clientId, + code: code, + scopes: scopes + )) } func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(for: .resetPasswordStart) - return makeRequest(url: url, method: "POST", json: [ - "username": username, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( + context: context, + target: .endpoint(.resetPasswordStart), + apiId: .telemetryApiIdV2ResetPassword, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, + username: username, + continuationToken: continuationToken + )) } func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try href.map { try resolver.url(forHref: $0) } ?? resolver.url(for: .signInStart) - return makeRequest(url: url, method: "POST", json: [ - "username": username, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( + context: context, + target: href.map { .href($0) } ?? .endpoint(.signInStart), + apiId: .telemetryApiIdV2SignIn, + operationType: MSALNativeAuthV2OperationType.signInStart.rawValue, + username: username, + continuationToken: continuationToken + )) } func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try href.map { try resolver.url(forHref: $0) } ?? resolver.url(for: .signUpStart) - return makeRequest(url: url, method: "POST", json: [ - "username": username, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( + context: context, + target: href.map { .href($0) } ?? .endpoint(.signUpStart), + apiId: .telemetryApiIdV2SignUp, + operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, + username: username, + continuationToken: continuationToken + )) } func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - return makeRequest(url: url, method: "POST", json: [ - "password": password, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.submitPassword.rawValue, + body: ["password": password, "continuationToken": continuationToken] + )) } func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - return makeRequest(url: url, method: "POST", json: [ - "code": code, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.submitCode.rawValue, + body: ["code": code, "continuationToken": continuationToken] + )) } func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - let body: [String: Any] = [ - "attributes": attributes, - "continuationToken": continuationToken - ] - return makeRequest(url: url, method: "POST", json: body, context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, + body: ["attributes": attributes, "continuationToken": continuationToken] + )) } func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - var body: [String: Any] = ["continuationToken": continuationToken] + var body: [AnyHashable: Any] = ["continuationToken": continuationToken] if let target = target { body["target"] = target } - return makeRequest(url: url, method: "POST", json: body, context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.registerMethod.rawValue, + body: body + )) } func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - return makeRequest(url: url, method: "POST", json: [ - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.challenge.rawValue, + body: ["continuationToken": continuationToken] + )) } func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - return makeRequest(url: url, method: "POST", json: [ - "otp": otp, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.verify.rawValue, + body: ["otp": otp, "continuationToken": continuationToken] + )) } func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - return makeRequest(url: url, method: "PUT", json: [ - "newPassword": newPassword, - "continuationToken": continuationToken - ], context: context) + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "PUT", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, + body: ["newPassword": newPassword, "continuationToken": continuationToken] + )) } func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - let url = try resolver.url(forHref: href) - return makeRequest(url: url, method: "POST", json: [ - "continuationToken": continuationToken - ], context: context) - } - - // MARK: - Request building - - private func makeRequest( - url: URL, - method: String, - json: [String: Any]? = nil, - form: [String: String]? = nil, - context: MSALNativeAuthRequestContext, - rawJSONResponse: Bool = false - ) -> MSIDHttpRequest { - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = method - - var headers: [String: String] = [:] - - if let json = json { - headers["Content-Type"] = "application/json" - urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: json) - } else if let form = form { - headers["Content-Type"] = "application/x-www-form-urlencoded" - urlRequest.httpBody = Self.encodeForm(form).data(using: .utf8) - } - - headers["Accept"] = "application/json" - headers["client-request-id"] = context.correlationId().uuidString - - let request = MSIDHttpRequest() - request.urlRequest = urlRequest - request.headers = headers - request.context = context - // The `/token` endpoint returns a plain OAuth response rather than HAL; leaving the response - // serializer unset makes `MSIDHttpRequest.send` hand back the raw JSON dictionary. - if !rawJSONResponse { - request.responseSerializer = MSALNativeAuthV2HALResponseSerializer() - } - request.errorHandler = MSALNativeAuthV2ResponseErrorHandler() - - if let interceptor = config.requestInterceptor { - request.requestInterceptor = MSALNativeAuthV2RequestInterceptorBridge(interceptor: interceptor) - } - - return request - } - - private static func encodeForm(_ parameters: [String: String]) -> String { - var allowed = CharacterSet.urlQueryAllowed - allowed.remove(charactersIn: "+&=") - return parameters - .map { key, value in - let encodedKey = key.addingPercentEncoding(withAllowedCharacters: allowed) ?? key - let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value - return "\(encodedKey)=\(encodedValue)" - } - .joined(separator: "&") - } -} - -/// Bridges `MSALNativeAuthRequestInterceptor` (Swift public protocol) to `MSIDHttpRequestInterceptorProtocol` (ObjC). -private final class MSALNativeAuthV2RequestInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { - - private let interceptor: MSALNativeAuthRequestInterceptor - - init(interceptor: MSALNativeAuthRequestInterceptor) { - self.interceptor = interceptor - } - - func addAdditionalHeaderFields( - for requestUrl: URL?, - with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock - ) { - interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in - completionBlock(additionalHeaders) - } + return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2Hal, + operationType: MSALNativeAuthV2OperationType.poll.rawValue, + body: ["continuationToken": continuationToken] + )) } } diff --git a/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift b/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift index aa1448c456..c17dc66fa2 100644 --- a/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift +++ b/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift @@ -29,6 +29,11 @@ public class MSALNativeAuthResetPasswordParameters: NSObject { /// username of the account to reset password. public var username: String + /// Permissions you want included in the access token received once the account is signed in + /// at the end of the reset password flow. + /// Not all scopes are guaranteed to be included in the access token returned. + public var scopes: [String]? + /// UUID to correlate this request with the server for debugging. public var correlationId: UUID? diff --git a/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift b/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift index 786f68f25d..f0577230cc 100644 --- a/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift +++ b/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift @@ -35,6 +35,11 @@ public class MSALNativeAuthSignUpParameters: NSObject { /// user attributes to be used during account creation. public var attributes: [String: Any]? + /// Permissions you want included in the access token received once the account is signed in + /// at the end of the sign up flow. + /// Not all scopes are guaranteed to be included in the access token returned. + public var scopes: [String]? + /// UUID to correlate this request with the server for debugging. public var correlationId: UUID? diff --git a/MSAL/src/native_auth/telemetry/MSALNativeAuthOperationTypes.swift b/MSAL/src/native_auth/telemetry/MSALNativeAuthOperationTypes.swift index 7126d1c478..eb3b6a7a8a 100644 --- a/MSAL/src/native_auth/telemetry/MSALNativeAuthOperationTypes.swift +++ b/MSAL/src/native_auth/telemetry/MSALNativeAuthOperationTypes.swift @@ -83,3 +83,20 @@ enum MSALNativeAuthSignOutType: MSALNativeAuthOperationType { case signOutAction = 0 case signOutForced = 1 } + +enum MSALNativeAuthV2OperationType: MSALNativeAuthOperationType { + case authorizeChallengeStart = 0 + case authorizeChallengeContinue = 1 + case token = 2 + case resetPasswordStart = 3 + case signInStart = 4 + case signUpStart = 5 + case challenge = 6 + case verify = 7 + case submitPassword = 8 + case submitCode = 9 + case submitAttributes = 10 + case registerMethod = 11 + case updatePassword = 12 + case poll = 13 +} diff --git a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift index 47e3f42ae2..2f39a25749 100644 --- a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift +++ b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift @@ -61,4 +61,11 @@ enum MSALNativeAuthTelemetryApiId: Int { case telemetryApiIdJITChallenge = 75030 case telemetryApiIdJITContinue = 75031 case telemetryApiISignInAfterJIT = 75032 + // Native Auth V2 (server-driven HAL) flows. + case telemetryApiIdV2AuthorizeChallenge = 76001 + case telemetryApiIdV2Token = 76002 + case telemetryApiIdV2SignIn = 76003 + case telemetryApiIdV2SignUp = 76004 + case telemetryApiIdV2ResetPassword = 76005 + case telemetryApiIdV2Hal = 76006 } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index 91447de7a6..260d515eaa 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -32,6 +32,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { private var requestProviderMock: MSALNativeAuthV2RequestProviderMock! private var validatorMock: MSALNativeAuthV2ResponseValidatorMock! private var cacheAccessorMock: MSALNativeAuthCacheAccessorMock! + private var resultFactoryMock: MSALNativeAuthResultFactoryMock! override func setUpWithError() throws { try super.setUpWithError() @@ -39,12 +40,14 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { requestProviderMock = .init() validatorMock = .init() cacheAccessorMock = .init() + resultFactoryMock = .init() sut = .init( config: MSALNativeAuthConfigStubs.configuration, requestProvider: requestProviderMock, responseValidator: validatorMock, - cacheAccessor: cacheAccessorMock + cacheAccessor: cacheAccessorMock, + resultFactory: resultFactoryMock ) } diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index fce615fb3a..4bc04aad6e 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -33,6 +33,7 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { private(set) var authorizeChallengeStartCalled = false private(set) var authorizeChallengeContinueCalled = false private(set) var tokenCalled = false + private(set) var tokenScopes: [String]? private(set) var resetPasswordStartCalled = false private(set) var signInStartCalled = false private(set) var signUpStartCalled = false @@ -77,8 +78,9 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { return try resolveRequest() } - func token(code: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { tokenCalled = true + tokenScopes = scopes return try resolveRequest() } From 1f5e0179a6a59538274aec1722687e52d48d0ae0 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 7 Jul 2026 17:34:25 +0100 Subject: [PATCH 10/85] Reorder items in PublicClientApplication --- ...SALNativeAuthPublicClientApplication.swift | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index c3908ee8c5..129685a4d5 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -264,22 +264,6 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic // MARK: - Native Auth V2 (server-driven) - /// Reset the password using the server-driven (V2) flow. - /// - Parameters: - /// - parameters: Parameters used for the Reset Password flow. - /// - delegate: Unified delegate that receives callbacks for the flow. - public func resetPasswordV2( - parameters: MSALNativeAuthResetPasswordParameters, - delegate: MSALNativeAuthFlowDelegate - ) { - let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) - let dispatcher = MSALNativeAuthFlowResponseDispatcher() - Task { - let response = await controller.resetPassword(parameters: parameters) - await dispatcher.dispatch(response, delegate: delegate) - } - } - /// Sign up a user using the server-driven (V2) flow. /// - Parameters: /// - parameters: Parameters used for the Sign Up flow. @@ -288,9 +272,10 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic parameters: MSALNativeAuthSignUpParameters, delegate: MSALNativeAuthFlowDelegate ) { - let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) - let dispatcher = MSALNativeAuthFlowResponseDispatcher() Task { + let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + let response = await controller.signUp(parameters: parameters) await dispatcher.dispatch(response, delegate: delegate) } @@ -304,14 +289,32 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic parameters: MSALNativeAuthSignInParameters, delegate: MSALNativeAuthFlowDelegate ) { - let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) - let dispatcher = MSALNativeAuthFlowResponseDispatcher() Task { + let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + let response = await controller.signIn(parameters: parameters) await dispatcher.dispatch(response, delegate: delegate) } } + /// Reset the password using the server-driven (V2) flow. + /// - Parameters: + /// - parameters: Parameters used for the Reset Password flow. + /// - delegate: Unified delegate that receives callbacks for the flow. + public func resetPasswordV2( + parameters: MSALNativeAuthResetPasswordParameters, + delegate: MSALNativeAuthFlowDelegate + ) { + Task { + let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + + let response = await controller.resetPassword(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) + } + } + /// Retrieve the current signed in account from the cache. /// - Parameter correlationId: Optional. UUID to correlate this request with the server for debugging. /// - Returns: An object representing the account information if present in the local cache. From 48ef139b7193b2aa26d8577e07eee96a3cdbbf44 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 10 Jul 2026 11:32:24 +0100 Subject: [PATCH 11/85] flow controller swiftlint generic swiftlint rules for AI --- .clinerules/04-Code-style-guidelines.md | 82 +++++ .clinerules/AGENTS.md | 2 + .../v2/MSALNativeAuthV2FlowController.swift | 338 ++++++++++++++---- 3 files changed, 344 insertions(+), 78 deletions(-) diff --git a/.clinerules/04-Code-style-guidelines.md b/.clinerules/04-Code-style-guidelines.md index 95801120f8..b2c407dbec 100644 --- a/.clinerules/04-Code-style-guidelines.md +++ b/.clinerules/04-Code-style-guidelines.md @@ -555,3 +555,85 @@ All new files **MUST** include the Microsoft copyright header when added to this ## Notes This style guide is adapted specifically for AI agents working on the Microsoft Authentication Library (MSAL) for iOS and macOS. When in doubt, prioritize consistency with existing codebase patterns over strict adherence to external style guides. + +--- + +## Swift Style (native_auth) + +The Swift code under `MSAL/src/native_auth` (including V2 / server-driven flows) **MUST** follow the same formatting conventions already established by the V1 native auth code (e.g. `MSALNativeAuthSignInController.swift`, `MSALNativeAuthResetPasswordController.swift`, `MSALNativeAuthSignUpController.swift`). SwiftLint is enforced for this directory via `MSAL/.swiftlint.yml`. + +### SwiftLint configuration (source of truth: `MSAL/.swiftlint.yml`) + +- `line_length`: warning at **150** columns. +- `type_name`: max length **60**. +- `function_parameter_count`: warning at **7**. +- Disabled rules: `todo`, `empty_enum_arguments`. +- Default limits apply for `function_body_length` (**50**), `cyclomatic_complexity` (**10**), `file_length`, and `type_body_length`. + +Changed native_auth Swift files **MUST** lint clean (zero warnings) before completion: + +```bash +swiftlint lint --quiet MSAL/src/native_auth/.swift +``` + +### Line length — WRAP, don't suppress + +When a call or declaration exceeds 150 columns, **wrap it** — put each argument on its own line, indented 4 spaces beyond the call, with the closing paren on its own line. This matches V1 (see `MSALNativeAuthResetPasswordController.swift` `doPollCompletionLoop` / `handlePollCompletionResponse`). + +```swift +// Preferred (V1 style) +return await mapInteraction( + startResult, + flowType: .signIn, + username: parameters.username, + scopes: scopes, + event: event, + context: context +) +``` + +Wrap long ternaries, `makeState(...)`, `response(.actionRequired(...))`, and `try self.requestProvider.foo(...)` calls the same way. For a long nested constructor, break the inner initializer onto its own lines too: + +```swift +return failure( + .error(MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "No usable sign-in method returned" + )), + event: event, + context: context +) +``` + +**Only** suppress `line_length` inline — `// swiftlint:disable:this line_length` — for an un-wrappable single string literal (log/error message). Never use it to avoid wrapping ordinary code. + +### Method / call declaration formatting + +- One parameter per line when a declaration exceeds the line limit; closing paren and `-> ReturnType` on their own line (matches V1 and the repo-wide Objective-C convention). +- 4-space indentation, never tabs. + +### function_body_length & cyclomatic_complexity — prefer suppression over refactor + +V1 controllers routinely exceed the 50-line body limit for legitimately long orchestration methods and suppress the warning rather than fragmenting the logic. Follow the same pattern — do **not** refactor control flow purely to satisfy the linter. + +- Add the suppression on the line immediately above the `func`: + + ```swift + // swiftlint:disable:next function_body_length + private func handleResponse(...) { ... } + ``` + +- When a function trips **both** rules, combine them on one line (see `MSALNativeAuthTokenResponseValidator.swift`): + + ```swift + // swiftlint:disable:next cyclomatic_complexity function_body_length + func validate(...) { ... } + ``` + +- For file- or type-level limits, use the block form at the top of the file / above the type: + + ```swift + // swiftlint:disable file_length + // swiftlint:disable:next type_body_length + final class MSALNativeAuth...Controller { ... } + ``` diff --git a/.clinerules/AGENTS.md b/.clinerules/AGENTS.md index 2ee99dc60d..aaee01fe71 100644 --- a/.clinerules/AGENTS.md +++ b/.clinerules/AGENTS.md @@ -34,6 +34,8 @@ Sample code snippets for both Swift & Objective-C can be found in the file `.cli Code style guidelines that AI agents MUST follow when working with this repository can be found in the file `.clinerules/04-Code-style-guidelines.md` +For Swift code under `MSAL/src/native_auth` (including V2 / server-driven flows), follow the **Swift Style (native_auth)** section of that file: match the existing V1 native auth formatting, keep changed files SwiftLint-clean (`MSAL/.swiftlint.yml`), wrap long calls/declarations to ≤150 columns (one argument per line) rather than suppressing `line_length`, and prefer `// swiftlint:disable:next function_body_length` / `cyclomatic_complexity` over refactoring long orchestration methods. + ## Feature flag guidelines Feature flag guidance for AI agents when implementing new features for MSAL library are defined in the file `.clinerules/05-Feature-gating.md` diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index aede240656..bae3795c23 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -26,12 +26,8 @@ import Foundation @_implementationOnly import MSAL_Private -/// Unified controller backing the Native Auth V2 (server-driven, HAL) flows. -/// -/// Mirrors the V1 controller structure (telemetry → request → validate → handle) but drives -/// the server-driven SSPR state machine end-to-end: bootstrap → reset-password start → -/// challenge → verify → update → poll → authorize-challenge → token. Sign up / sign in V2 are -/// defined for the unified contract but return `notImplemented` until their server APIs ship. +// swiftlint:disable file_length +// swiftlint:disable:next type_body_length final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNativeAuthV2FlowControlling { private let config: MSALNativeAuthInternalConfiguration @@ -40,7 +36,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa private let cacheAccessor: MSALNativeAuthCacheInterface private let resultFactory: MSALNativeAuthResultBuildable - private let maxPollAttempts = 5 + private let kNumberOfTimesToRetryPollCompletionCall = 5 private let pollIntervalNanoseconds: UInt64 = 1_500_000_000 // 1.5s init( @@ -70,38 +66,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Entry points - func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { - let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) - let scopes = joinScopes(parameters.scopes) - - // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token). - let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let bootstrapToken, _) = bootstrap else { - return failure(bootstrap, event: event, context: context) - } - - // Step 2 — reset-password start. - let startResult = await performInteraction(context: context) { - try self.requestProvider.resetPasswordStart(username: parameters.username, continuationToken: bootstrapToken, context: context) - } - - guard case .challengeRequired(let token2, let challengeHref, let hint) = startResult else { - return interactionFailure(startResult, event: event, context: context, newState: nil) - } - - // Step 3 — auto-trigger the challenge (send EOTP). - guard let challengeHref = challengeHref else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link")), event: event, context: context) - } - - let challengeResult = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) - } - - return handleCodeRequired(challengeResult, username: parameters.username, fallbackHint: hint, scopes: scopes, event: event, context: context) - } - func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUp, context: context) @@ -126,9 +90,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return await mapInteraction(startResult, flowType: .signUp, username: parameters.username, scopes: scopes, event: event, context: context) } + // swiftlint:disable:next function_body_length func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) - let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil ? .telemetryApiIdSignInWithPasswordStart : .telemetryApiIdSignInWithCodeStart + let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil + ? .telemetryApiIdSignInWithPasswordStart + : .telemetryApiIdSignInWithCodeStart let event = makeAndStartTelemetryEvent(id: apiId, context: context) let scopes = joinScopes(parameters.scopes) @@ -168,18 +135,41 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } guard let method = chosen, let href = method.links["challenge"] else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "No usable sign-in method returned")), event: event, context: context) + return failure( + .error( + MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "No usable sign-in method returned" + ) + ), + event: event, + context: context + ) } token2 = token challengeHref = href case .challengeRequired(let token, let href, _): guard let href = href else { - return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) + return await mapInteraction( + startResult, + flowType: .signIn, + username: parameters.username, + scopes: scopes, + event: event, + context: context + ) } token2 = token challengeHref = href default: - return await mapInteraction(startResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) + return await mapInteraction( + startResult, + flowType: .signIn, + username: parameters.username, + scopes: scopes, + event: event, + context: context + ) } // Step 4 — challenge the chosen method. @@ -193,16 +183,63 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let verifyResult = await performInteraction(context: context) { try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) } - return await mapInteraction(verifyResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) + return await mapInteraction( + verifyResult, + flowType: .signIn, + username: parameters.username, + scopes: scopes, + event: event, + context: context + ) } - return await mapInteraction(challengeResult, flowType: .signIn, username: parameters.username, scopes: scopes, event: event, context: context) + return await mapInteraction( + challengeResult, + flowType: .signIn, + username: parameters.username, + scopes: scopes, + event: event, + context: context + ) } - // MARK: - Continuation -} + func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) + let scopes = joinScopes(parameters.scopes) -extension MSALNativeAuthV2FlowController { + // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token). + let bootstrap = await performAuthorizeChallengeStart(context: context) + guard case .continuationToken(let bootstrapToken, _) = bootstrap else { + return failure(bootstrap, event: event, context: context) + } + + // Step 2 — reset-password start. + let startResult = await performInteraction(context: context) { + try self.requestProvider.resetPasswordStart(username: parameters.username, continuationToken: bootstrapToken, context: context) + } + + guard case .challengeRequired(let token2, let challengeHref, let hint) = startResult else { + return interactionFailure(startResult, event: event, context: context, newState: nil) + } + + // Step 3 — auto-trigger the challenge (send EOTP). + guard let challengeHref = challengeHref else { + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link")), + event: event, + context: context + ) + } + + let challengeResult = await performInteraction(context: context) { + try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) + } + + return handleCodeRequired(challengeResult, username: parameters.username, fallbackHint: hint, scopes: scopes, event: event, context: context) + } + + // MARK: - Continuation func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) @@ -210,18 +247,32 @@ extension MSALNativeAuthV2FlowController { guard let verifyHref = continuation.link("verify")?.absoluteString else { let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + event: event, + context: context + ) } // Sign in / sign up use `code`; reset password uses `otp`. switch continuation.flowType { case .signIn, .signUp: - let apiId: MSALNativeAuthTelemetryApiId = continuation.flowType == .signUp ? .telemetryApiIdSignUpSubmitCode : .telemetryApiIdSignInSubmitCode + let apiId: MSALNativeAuthTelemetryApiId = continuation.flowType == .signUp + ? .telemetryApiIdSignUpSubmitCode + : .telemetryApiIdSignInSubmitCode let event = makeAndStartTelemetryEvent(id: apiId, context: context) let result = await performInteraction(context: context) { try self.requestProvider.submitCode(href: verifyHref, code: code, continuationToken: continuation.continuationToken, context: context) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context, + recoverableState: state + ) case .resetPassword: let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) let result = await performInteraction(context: context) { @@ -229,7 +280,13 @@ extension MSALNativeAuthV2FlowController { } switch result { case .updateRequired(let token, let updateHref): - let newState = makeState(.resetPassword, continuationToken: token, links: ["update": updateHref], username: continuation.username, scopes: continuation.scopes) + let newState = makeState( + .resetPassword, + continuationToken: token, + links: ["update": updateHref], + username: continuation.username, + scopes: continuation.scopes + ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) case .error(let error): @@ -247,27 +304,54 @@ extension MSALNativeAuthV2FlowController { let continuation = state.continuation guard let verifyHref = continuation.link("verify")?.absoluteString else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + event: event, + context: context + ) } let result = await performInteraction(context: context) { - try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.submitPassword( + href: verifyHref, + password: password, + continuationToken: continuation.continuationToken, + context: context + ) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context, + recoverableState: state + ) } + // swiftlint:disable:next cyclomatic_complexity function_body_length func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmit, context: context) let continuation = state.continuation guard let updateHref = continuation.link("update")?.absoluteString else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing update link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing update link")), + event: event, + context: context + ) } // Step 5 — update password. let updateResult = await performInteraction(context: context) { - try self.requestProvider.updatePassword(href: updateHref, newPassword: password, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.updatePassword( + href: updateHref, + newPassword: password, + continuationToken: continuation.continuationToken, + context: context + ) } guard case .pollInProgress(var pollToken, let pollHref) = updateResult else { @@ -275,12 +359,16 @@ extension MSALNativeAuthV2FlowController { } guard let pollHref = pollHref else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing poll link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing poll link")), + event: event, + context: context + ) } // Step 6 — poll until the operation completes. var completionToken: String? - for attempt in 0.. 0 { try? await Task.sleep(nanoseconds: pollIntervalNanoseconds) } @@ -307,10 +395,25 @@ extension MSALNativeAuthV2FlowController { } guard let completionToken = completionToken else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Password reset did not complete in time")), event: event, context: context) + return failure( + .error( + MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Password reset did not complete in time" + ) + ), + event: event, + context: context + ) } - return await completeWithToken(continuationToken: completionToken, username: continuation.username, scopes: continuation.scopes, event: event, context: context) + return await completeWithToken( + continuationToken: completionToken, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context + ) } func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -319,15 +422,33 @@ extension MSALNativeAuthV2FlowController { let continuation = state.continuation guard let submitHref = continuation.link("submitAttributes")?.absoluteString else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing submit-attributes link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing submit-attributes link")), + event: event, + context: context + ) } let result = await performInteraction(context: context) { - try self.requestProvider.submitAttributes(href: submitHref, attributes: attributes, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.submitAttributes( + href: submitHref, + attributes: attributes, + continuationToken: continuation.continuationToken, + context: context + ) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context, + recoverableState: state + ) } + // swiftlint:disable:next function_body_length func selectAuthMethod( _ method: MSALAuthMethod, verificationContact: String?, @@ -341,14 +462,35 @@ extension MSALNativeAuthV2FlowController { let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link("enroll"))?.absoluteString { let event = makeAndStartTelemetryEvent(id: .telemetryApiIdJITChallenge, context: context) let result = await performInteraction(context: context) { - try self.requestProvider.registerMethod(href: enrollHref, target: verificationContact, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.registerMethod( + href: enrollHref, + target: verificationContact, + continuationToken: continuation.continuationToken, + context: context + ) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context) + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context + ) } let event = makeAndStartTelemetryEvent(id: .telemetryApiIdMFAGetAuthMethods, context: context) guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link("challenge"))?.absoluteString else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link for selected method")), event: event, context: context) + return failure( + .error( + MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Missing challenge link for selected method" + ) + ), + event: event, + context: context + ) } let result = await performInteraction(context: context) { @@ -373,7 +515,14 @@ extension MSALNativeAuthV2FlowController { newState: newState ), context: context) default: - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context) + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context + ) } } @@ -384,13 +533,30 @@ extension MSALNativeAuthV2FlowController { // JIT activation uses the `activate` link; MFA uses the `verify` link. guard let submitHref = (continuation.link("activate") ?? continuation.link("verify"))?.absoluteString else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify/activate link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify/activate link")), + event: event, + context: context + ) } let result = await performInteraction(context: context) { - try self.requestProvider.submitCode(href: submitHref, code: challenge, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.submitCode( + href: submitHref, + code: challenge, + continuationToken: continuation.continuationToken, + context: context + ) } - return await mapInteraction(result, flowType: continuation.flowType, username: continuation.username, scopes: continuation.scopes, event: event, context: context, recoverableState: state) + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context, + recoverableState: state + ) } func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { @@ -399,14 +565,25 @@ extension MSALNativeAuthV2FlowController { let continuation = state.continuation guard let resendHref = continuation.link("resend")?.absoluteString else { - return failure(.error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing resend link")), event: event, context: context) + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing resend link")), + event: event, + context: context + ) } let result = await performInteraction(context: context) { try self.requestProvider.challenge(href: resendHref, continuationToken: continuation.continuationToken, context: context) } - return handleCodeRequired(result, username: continuation.username, fallbackHint: continuation.sentToHint, scopes: continuation.scopes, event: event, context: context) + return handleCodeRequired( + result, + username: continuation.username, + fallbackHint: continuation.sentToHint, + scopes: continuation.scopes, + event: event, + context: context + ) } // MARK: - Shared step helpers @@ -489,9 +666,10 @@ extension MSALNativeAuthV2FlowController { } } - /// Maps a validated interaction response onto a controller response (the unified, server-driven - /// branch used by sign in / sign up / MFA / JIT continuation steps). On a terminal `continue` - /// state it runs the completion (authorize-challenge → token) sequence. + // Maps a validated interaction response onto a controller response (the unified, server-driven + // branch used by sign in / sign up / MFA / JIT continuation steps). On a terminal `continue` + // state it runs the completion (authorize-challenge → token) sequence. + // swiftlint:disable:next function_body_length private func mapInteraction( _ result: MSALNativeAuthV2InteractionValidatedResponse, flowType: MSALNativeAuthV2FlowType, @@ -577,7 +755,10 @@ extension MSALNativeAuthV2FlowController { ), context: context) case .error(let error): stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: error.isInvalidCode || error.kind == .invalidPassword ? recoverableState : nil), context: context) + return response( + .error(error: error, newState: error.isInvalidCode || error.kind == .invalidPassword ? recoverableState : nil), + context: context + ) default: return interactionFailure(result, event: event, context: context, newState: nil) } @@ -619,7 +800,8 @@ extension MSALNativeAuthV2FlowController { return response(.error(error: flowError, newState: nil), context: context) } case .failure(let error): - let flowError = (error as? MSALNativeAuthFlowError) ?? MSALNativeAuthFlowError(kind: .generalError, errorDescription: (error as NSError).localizedDescription) + let flowError = (error as? MSALNativeAuthFlowError) + ?? MSALNativeAuthFlowError(kind: .generalError, errorDescription: (error as NSError).localizedDescription) stopTelemetryEvent(event, context: context, error: flowError) return response(.error(error: flowError, newState: nil), context: context) } From 645db2b4e7b77762225afc885526925188c8bcc5 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 10 Jul 2026 11:37:28 +0100 Subject: [PATCH 12/85] Swiftlint --- .../network/responses/v2/HALResource.swift | 53 ++++++------------- ...MSALNativeAuthV2ResponseErrorHandler.swift | 1 + .../MSALNativeAuthV2ResponseValidator.swift | 6 ++- .../MSALNativeAuthV2ValidatedResponses.swift | 1 + .../v2/MSALNativeAuthV2RequestProvider.swift | 21 ++++++-- 5 files changed, 42 insertions(+), 40 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/HALResource.swift b/MSAL/src/native_auth/network/responses/v2/HALResource.swift index e08a48a9d7..849666d96a 100644 --- a/MSAL/src/native_auth/network/responses/v2/HALResource.swift +++ b/MSAL/src/native_auth/network/responses/v2/HALResource.swift @@ -27,8 +27,7 @@ import Foundation /// Represents a HAL Link Object as defined by the JSON-HAL specification. /// /// See: https://www.ietf.org/archive/id/draft-kelly-json-hal-11.html -internal struct HALLink -{ +internal struct HALLink { /// The URI of the linked resource. let href: String @@ -38,16 +37,14 @@ internal struct HALLink /// Whether `href` is a URI Template (RFC 6570). let templated: Bool - init(href: String, name: String? = nil, templated: Bool = false) - { + init(href: String, name: String? = nil, templated: Bool = false) { self.href = href self.name = name self.templated = templated } /// Parses a HAL Link Object from a JSON dictionary. - init?(json: [String: Any]) - { + init?(json: [String: Any]) { guard let href = json["href"] as? String else { return nil } self.href = href self.name = json["name"] as? String @@ -59,8 +56,7 @@ internal struct HALLink /// /// Handles extraction of `_links` and `_embedded` sections, /// and provides typed accessors for common HAL patterns. -internal struct HALResource -{ +internal struct HALResource { /// The raw JSON properties (excluding `_links` and `_embedded`). let properties: [String: Any] @@ -71,25 +67,19 @@ internal struct HALResource let embedded: [String: [[String: Any]]] /// Parses a HAL resource from a JSON dictionary. - init(json: [String: Any]) - { + init(json: [String: Any]) { var props = json var parsedLinks: [String: [HALLink]] = [:] var parsedEmbedded: [String: [[String: Any]]] = [:] // Parse _links - if let linksJson = json["_links"] as? [String: Any] - { - for (rel, value) in linksJson - { + if let linksJson = json["_links"] as? [String: Any] { + for (rel, value) in linksJson { if rel == "curies" { continue } - if let linkDict = value as? [String: Any], let link = HALLink(json: linkDict) - { + if let linkDict = value as? [String: Any], let link = HALLink(json: linkDict) { parsedLinks[rel] = [link] - } - else if let linkArray = value as? [[String: Any]] - { + } else if let linkArray = value as? [[String: Any]] { parsedLinks[rel] = linkArray.compactMap { HALLink(json: $0) } } } @@ -97,16 +87,11 @@ internal struct HALResource } // Parse _embedded - if let embeddedJson = json["_embedded"] as? [String: Any] - { - for (rel, value) in embeddedJson - { - if let array = value as? [[String: Any]] - { + if let embeddedJson = json["_embedded"] as? [String: Any] { + for (rel, value) in embeddedJson { + if let array = value as? [[String: Any]] { parsedEmbedded[rel] = array - } - else if let single = value as? [String: Any] - { + } else if let single = value as? [String: Any] { parsedEmbedded[rel] = [single] } } @@ -121,26 +106,22 @@ internal struct HALResource // MARK: - Accessors /// Returns a single link for the given relation, or nil if not present. - func link(rel: String) -> HALLink? - { + func link(rel: String) -> HALLink? { return links[rel]?.first } /// Returns all links for the given relation. - func allLinks(rel: String) -> [HALLink] - { + func allLinks(rel: String) -> [HALLink] { return links[rel] ?? [] } /// Returns embedded resources for the given relation. - func embeddedResources(rel: String) -> [[String: Any]] - { + func embeddedResources(rel: String) -> [[String: Any]] { return embedded[rel] ?? [] } /// Returns a string property value. - func string(forKey key: String) -> String? - { + func string(forKey key: String) -> String? { return properties[key] as? String } } diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift index 7cb1b90e82..5f3006b9e2 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift @@ -36,6 +36,7 @@ import Foundation /// back to the caller; the V2 validator (not the transport) decides success vs failure. final class MSALNativeAuthV2ResponseErrorHandler: NSObject, MSIDHttpRequestErrorHandling { + // swiftlint:disable:next function_parameter_count func handleError( _ error: Error?, httpResponse: HTTPURLResponse?, diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 409f79fb6b..4cebfc855f 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -49,10 +49,14 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin if let continuationToken = response.continuationToken { return .continuationToken(continuationToken: continuationToken, links: response.links) } - return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "authorize-challenge returned neither a continuation token nor a code")) + return .error(MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "authorize-challenge returned neither a continuation token nor a code" + )) } } + // swiftlint:disable:next cyclomatic_complexity function_body_length func validateInteraction( _ result: Result ) -> MSALNativeAuthV2InteractionValidatedResponse { diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index 373c026d33..3b00f91e28 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -76,6 +76,7 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { case readyToComplete(continuationToken: String) case error(MSALNativeAuthFlowError) + // swiftlint:disable:next cyclomatic_complexity static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case let (.signInMethods(lToken, lMethods), .signInMethods(rToken, rMethods)): diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index b40b16ef8b..4bcc740957 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -54,7 +54,12 @@ protocol MSALNativeAuthV2RequestProviding { func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest /// Submit collected attributes (sign up) to a server `submitAttributes` href. - func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + func submitAttributes( + href: String, + attributes: [String: Any], + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest /// Register a strong-auth method (JIT) by posting the target to a server `enroll` href. func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest @@ -174,7 +179,12 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func submitAttributes( + href: String, + attributes: [String: Any], + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, @@ -222,7 +232,12 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func updatePassword( + href: String, + newPassword: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, From 2590b884db9a79a90d37cbb80b8fe5d170afa031 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 10 Jul 2026 14:58:52 +0100 Subject: [PATCH 13/85] comments --- .../v2/MSALNativeAuthV2FlowController.swift | 16 +++++++------- .../v2/MSALNativeAuthV2FlowResult.swift | 2 +- ...SALNativeAuthV2HALResponseSerializer.swift | 8 +++---- .../MSALNativeAuthV2ResponseValidator.swift | 6 +++--- .../v2/MSALNativeAuthV2HrefURLResolver.swift | 2 +- .../MSALNativeAuthV2RequestConfigurator.swift | 21 +++++++++---------- .../v2/MSALNativeAuthV2RequestProvider.swift | 9 ++++---- .../v2/MSALNativeAuthFlowDelegate.swift | 3 +-- 8 files changed, 32 insertions(+), 35 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index bae3795c23..f4c85df868 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -178,7 +178,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } // Step 5 — when a password was supplied and the password factor is required, submit it now - // so password sign-in completes in one call (mirroring V1). + // so password sign-in completes in one call. if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult, let verifyHref = verifyHref { let verifyResult = await performInteraction(context: context) { try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) @@ -765,9 +765,8 @@ extension MSALNativeAuthV2FlowController { } /// Completion sequence shared by every flow: authorize-challenge (continue) → token exchange. - /// The `/token` response is persisted to the shared MSAL token cache — exactly like the V1 - /// sign-in flow — so the returned ``MSALNativeAuthUserAccountResult`` can vend access tokens - /// via `getAccessToken(...)`. + /// The `/token` response is persisted to the shared MSAL token cache so the returned + /// ``MSALNativeAuthUserAccountResult`` can retrieve access tokens via `getAccessToken(...)`. private func completeWithToken( continuationToken: String, username: String?, @@ -820,6 +819,8 @@ extension MSALNativeAuthV2FlowController { return .failure(error) } + let fallbackCorrelationId = request.context?.correlationId().uuidString + return await withCheckedContinuation { continuation in request.send { response, error in if let error = error { @@ -832,7 +833,7 @@ extension MSALNativeAuthV2FlowController { } do { let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) - tokenResponse.correlationId = tokenResponse.correlationId ?? request.context?.correlationId().uuidString + tokenResponse.correlationId = tokenResponse.correlationId ?? fallbackCorrelationId continuation.resume(returning: .success(tokenResponse)) } catch { continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) @@ -842,7 +843,7 @@ extension MSALNativeAuthV2FlowController { } /// Persists the token response (tokens + account) to the shared MSAL cache and returns the - /// resulting `MSIDTokenResult`. Mirrors the V1 `cacheTokenResponse` implementation. + /// resulting `MSIDTokenResult`. private func cacheTokenResponse( _ tokenResponse: MSIDTokenResponse, context: MSALNativeAuthRequestContext, @@ -915,8 +916,7 @@ extension MSALNativeAuthV2FlowController { } /// Merges the caller-requested scopes with the default OIDC scopes (openid, profile, - /// offline_access), de-duplicating while preserving order. Mirrors the V1 sign-in behaviour so - /// the same access token / cache target results regardless of the flow version. + /// offline_access), de-duplicating while preserving order. private func joinScopes(_ scopes: [String]?) -> [String] { let defaultOIDCScopes = MSALPublicClientApplication.defaultOIDCScopes().array guard let scopes = scopes else { diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift index b86eb5a2a5..4fc094dcfd 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -47,7 +47,7 @@ struct MSALNativeAuthV2ContinuationState { /// Auth methods offered for MFA / strong-auth (JIT) selection. let authMethods: [MSALAuthMethod] /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final - /// `/token` exchange. Threaded through every step so completion mirrors the V1 sign-in flow. + /// `/token` exchange. Threaded through every step. let scopes: [String] init( diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index c80f732011..d3006a5569 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -28,10 +28,10 @@ import Foundation /// Parses a raw HTTP response into a ``MSALNativeAuthHALResponse``. /// -/// Unlike the V1 serializer (Decodable + snake_case), V2 responses are HAL+JSON and -/// every HTTP outcome carries a meaningful body, so this serializer never throws on a -/// non-200 status — it captures the status code and lets the V2 validator decide. HAL -/// `_links` / `_embedded` extraction is delegated to the shared `HALResource`. +/// V2 responses are HAL+JSON and every HTTP outcome carries a meaningful body, so this +/// serializer never throws on a non-200 status — it captures the status code and lets the +/// V2 validator decide. HAL `_links` / `_embedded` extraction is delegated to the shared +/// `HALResource`. final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSerialization { func responseObject(for httpResponse: HTTPURLResponse?, data: Data?, context: MSIDRequestContext?) throws -> Any { diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 4cebfc855f..c9d2b4d04b 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -177,8 +177,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin kind = .userNotFound } else if serverError.innerErrorCode == "invalidUserNameOrPassword" || errorCodes.contains(MSALNativeAuthESTSApiErrorCodes.invalidCredentials.rawValue) { - // Wrong username/password at sign in (AADSTS50126). Mirrors the V1 `.invalidCredentials` - // handling: a recoverable credentials error, not an invalid one-time code. + // Wrong username/password at sign in (AADSTS50126): a recoverable credentials error, + // not an invalid one-time code. kind = .invalidPassword } else if serverError.code == "invalidGrant" { kind = .invalidCode @@ -195,7 +195,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin } /// Extracts the numeric ESTS error codes (e.g. `50126` from `AADSTS50126`) embedded in a - /// server error message, mirroring the `error_codes` array the V1 flows surface. + /// server error message. private static func estsErrorCodes(from message: String?) -> [Int] { guard let message = message else { return [] diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 7f844371e3..f25ec8beb6 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -30,7 +30,7 @@ import Foundation /// hrefs may be absolute, or relative/templated (e.g. `{tenant}/api/v0.1/auth/...`). This /// resolver normalises a server href against the configured authority host, and also builds /// URLs for the fixed ``MSALNativeAuthV2Endpoint`` cases. The slice/data-center query -/// parameter is appended consistently, mirroring the V1 `makeEndpointUrl` behaviour. +/// parameter is appended consistently. struct MSALNativeAuthV2HrefURLResolver { private let authorityURL: URL diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift index e045947a0a..3ceb34b136 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -28,11 +28,10 @@ import Foundation // MARK: - Request parameters -/// Describes a single V2 native-auth request. This mirrors the role of V1's `MSALNativeAuthRequestable`: -/// each concrete parameter type knows its target URL, HTTP method, body, body encoding and telemetry -/// identity. `MSALNativeAuthV2RequestConfigurator` turns any of these into a fully-configured -/// `MSIDHttpRequest` that reuses the shared AAD request pipeline (device-id headers, PkeyAuth, -/// correlation, server telemetry) — so V2 requests carry the same headers as V1. +/// Describes a single V2 native-auth request: each concrete parameter type knows its target URL, +/// HTTP method, body, body encoding and telemetry identity. `MSALNativeAuthV2RequestConfigurator` +/// turns any of these into a fully-configured `MSIDHttpRequest` that reuses the shared AAD request +/// pipeline (device-id headers, PkeyAuth, correlation, server telemetry). protocol MSALNativeAuthV2Requestable { var context: MSALNativeAuthRequestContext { get } var httpMethod: String { get } @@ -168,11 +167,11 @@ struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { // MARK: - Request configurator -/// Builds a fully-configured `MSIDHttpRequest` for any `MSALNativeAuthV2Requestable`, reusing the same -/// shared AAD request pipeline as V1 native auth. Subclassing `MSIDAADRequestConfigurator` gives V2 the -/// standard device-id (`x-client-*`) headers, app metadata, PkeyAuth, `Accept: application/json`, -/// correlation headers and the authority network-host rewrite. On top of that it attaches the V2 HAL / -/// raw-JSON response serializer, the V2 error handler, server telemetry and the request interceptor. +/// Builds a fully-configured `MSIDHttpRequest` for any `MSALNativeAuthV2Requestable`. Subclassing +/// `MSIDAADRequestConfigurator` gives V2 the standard device-id (`x-client-*`) headers, app metadata, +/// PkeyAuth, `Accept: application/json`, correlation headers and the authority network-host rewrite. +/// On top of that it attaches the V2 HAL / raw-JSON response serializer, the V2 error handler, server +/// telemetry and the request interceptor. final class MSALNativeAuthV2RequestConfigurator: MSIDAADRequestConfigurator { private let config: MSALNativeAuthInternalConfiguration @@ -206,7 +205,7 @@ final class MSALNativeAuthV2RequestConfigurator: MSIDAADRequestConfigurator { encoding: parameters.encoding ) - // Reuse the shared AAD request pipeline (same base configuration V1 native auth relies on). + // Reuse the shared AAD request pipeline. configure(request) // `MSIDAADRequestConfigurator` writes the standard headers onto `urlRequest`, but the native diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 4bcc740957..27f7c79d40 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -105,13 +105,12 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { // The authorization_code exchange sends client_id, code, grant_type and the requested - // `scope` (caller scopes merged with the default OIDC scopes), mirroring the V1 sign-in - // token call. `scope` is sent here — not on the authorize-challenge bootstrap — because - // including it in the bootstrap makes ESTS mint a continuation token the start endpoints - // reject with AADSTS55200. + // `scope` (caller scopes merged with the default OIDC scopes). `scope` is sent here — not + // on the authorize-challenge bootstrap — because including it in the bootstrap makes ESTS + // mint a continuation token the start endpoints reject with AADSTS55200. // The `/token` endpoint returns a standard OAuth token response (NOT HAL), so this request // yields the raw JSON dictionary for the controller to parse into an `MSIDTokenResponse` and - // persist to the cache (mirroring the V1 sign-in flow). + // persist to the cache. // `client_info=true` (added by the parameter class) asks ESTS to return the `client_info` // blob (uid/utid) in the token response. IdentityCore's AAD-v2/CIAM factory rejects any // token response without it ("Client info was not returned in the server response"), which diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift index 237452fe57..c6a45e92dd 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift @@ -26,8 +26,7 @@ import Foundation /// Single unified delegate for all Native Auth V2 (server-driven) flows. /// -/// Unlike V1 — which exposes a different delegate protocol per step — V2 uses one -/// delegate for sign up, sign in and reset password. The SDK drives the flow and +/// One delegate serves sign up, sign in and reset password. The SDK drives the flow and /// reports back through these callbacks; the app reacts and continues the flow by /// calling methods on the provided ``MSALNativeAuthFlowState``. /// From 17e18ed2749aceff9b835dd3421fc95a1ed95e26 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 10 Jul 2026 16:05:42 +0100 Subject: [PATCH 14/85] Native Auth V2: early link validation, non-optional hrefs, method ordering, comment cleanup Fail fast on missing bootstrap links, make provider hrefs non-optional, consistently order request-provider methods, and strip step-numbered / V1-comparison narration comments. - Controller entry points (signUp/signIn/resetPassword) now resolve their required bootstrap link (sign_up/sign_in/reset_password) right after the authorize-challenge guard and fail early via missingLinkFailure(...) with an "improper response" error, instead of passing an optional href that only fails on the next request. Renamed the local bootstrapToken -> continuationToken. - MSALNativeAuthV2RequestProviding: signUpStart/signInStart/resetPasswordStart now take a non-optional href (always .href(href)); resetPasswordStart uses its bootstrap reset_password link. Removed the now-dead signInStart/signUpStart/ resetPasswordStart cases from MSALNativeAuthV2Endpoint. - Reordered all V2 request-provider methods (protocol + impl) to signUp -> signIn -> resetPassword -> HAL methods -> authorize-challenge (start/continue) -> token (last). Paren-aligned multi-parameter signatures. - Removed step-numbered ("Step 1 - ...", "(step 7)") and V1-comparison comments across the V2 layer; comments now describe what each piece does without implying a fixed call order. - Updated the request-provider mock and one reset-password test to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v2/MSALNativeAuthV2FlowController.swift | 70 +++-- .../v2/MSALNativeAuthHALResponse.swift | 4 +- .../MSALNativeAuthV2ValidatedResponses.swift | 8 +- .../network/v2/MSALNativeAuthV2Endpoint.swift | 13 +- .../v2/MSALNativeAuthV2RequestProvider.swift | 249 +++++++++++------- .../MSALNativeAuthV2FlowControllerTests.swift | 2 +- .../MSALNativeAuthV2RequestProviderMock.swift | 6 +- 7 files changed, 215 insertions(+), 137 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index f4c85df868..06e56868f2 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -71,18 +71,21 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUp, context: context) let scopes = joinScopes(parameters.scopes) - // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token + sign_up link). + // Bootstrap authorize-challenge (expects 401 + continuation token + sign_up link). let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let bootstrapToken, let links) = bootstrap else { + guard case .continuationToken(let continuationToken, let links) = bootstrap else { return failure(bootstrap, event: event, context: context) } + guard let signUpLink = links["sign_up"] ?? links["signup"] else { + return missingLinkFailure(relation: "sign_up", event: event, context: context) + } - // Step 2 — sign-up start (auto-triggers the email challenge). + // Sign-up start (auto-triggers the email challenge). let startResult = await performInteraction(context: context) { try self.requestProvider.signUpStart( username: parameters.username, - continuationToken: bootstrapToken, - href: links["sign_up"] ?? links["signup"], + continuationToken: continuationToken, + href: signUpLink, context: context ) } @@ -99,23 +102,26 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let event = makeAndStartTelemetryEvent(id: apiId, context: context) let scopes = joinScopes(parameters.scopes) - // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token + sign_in link). + // Bootstrap authorize-challenge (expects 401 + continuation token + sign_in link). let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let bootstrapToken, let links) = bootstrap else { + guard case .continuationToken(let continuationToken, let links) = bootstrap else { return failure(bootstrap, event: event, context: context) } + guard let signInLink = links["sign_in"] ?? links["signin"] else { + return missingLinkFailure(relation: "sign_in", event: event, context: context) + } - // Step 2 — sign-in (method discovery). + // Sign-in (method discovery). let startResult = await performInteraction(context: context) { try self.requestProvider.signInStart( username: parameters.username, - continuationToken: bootstrapToken, - href: links["sign_in"] ?? links["signin"], + continuationToken: continuationToken, + href: signInLink, context: context ) } - // Step 3 — resolve the token and the method challenge href. + // Resolve the token and the method challenge href. // The server may either return `.signInMethods` (action == nil, methods embedded) so the // client picks a method, or collapse discovery and return `.challengeRequired` (action == // "challenge") directly with the chosen method's challenge href already resolved. @@ -172,12 +178,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Step 4 — challenge the chosen method. + // Challenge the chosen method. let challengeResult = await performInteraction(context: context) { try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) } - // Step 5 — when a password was supplied and the password factor is required, submit it now + // When a password was supplied and the password factor is required, submit it now // so password sign-in completes in one call. if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult, let verifyHref = verifyHref { let verifyResult = await performInteraction(context: context) { @@ -208,22 +214,30 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) let scopes = joinScopes(parameters.scopes) - // Step 1 — bootstrap authorize-challenge (expects 401 + continuation token). + // Bootstrap authorize-challenge (expects 401 + continuation token + reset_password link). let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let bootstrapToken, _) = bootstrap else { + guard case .continuationToken(let continuationToken, let links) = bootstrap else { return failure(bootstrap, event: event, context: context) } + guard let resetPasswordLink = links["reset_password"] ?? links["resetpassword"] else { + return missingLinkFailure(relation: "reset_password", event: event, context: context) + } - // Step 2 — reset-password start. + // Reset-password start. let startResult = await performInteraction(context: context) { - try self.requestProvider.resetPasswordStart(username: parameters.username, continuationToken: bootstrapToken, context: context) + try self.requestProvider.resetPasswordStart( + username: parameters.username, + continuationToken: continuationToken, + href: resetPasswordLink, + context: context + ) } guard case .challengeRequired(let token2, let challengeHref, let hint) = startResult else { return interactionFailure(startResult, event: event, context: context, newState: nil) } - // Step 3 — auto-trigger the challenge (send EOTP). + // Auto-trigger the challenge (send EOTP). guard let challengeHref = challengeHref else { return failure( .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link")), @@ -344,7 +358,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Step 5 — update password. + // Update password. let updateResult = await performInteraction(context: context) { try self.requestProvider.updatePassword( href: updateHref, @@ -366,7 +380,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Step 6 — poll until the operation completes. + // Poll until the operation completes. var completionToken: String? for attempt in 0.. 0 { @@ -629,9 +643,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } // MARK: - Result mapping -} - -extension MSALNativeAuthV2FlowController { private func handleCodeRequired( _ result: MSALNativeAuthV2InteractionValidatedResponse, @@ -987,6 +998,19 @@ extension MSALNativeAuthV2FlowController { return response(.error(error: error, newState: nil), context: context) } + private func missingLinkFailure( + relation: String, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext + ) -> MSALNativeAuthV2FlowControllerResponse { + let error = MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Invalid authorize-challenge response: missing '\(relation)' link" + ) + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context) + } + private func interactionFailure( _ validated: MSALNativeAuthV2InteractionValidatedResponse, event: MSIDTelemetryAPIEvent?, diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 60201617d8..da9b2924f5 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -76,9 +76,9 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { /// Attributes the server requests on a sign up `collectAttributes` response. let attributes: [RequiredAttributeEntry] - /// Authorization code from the final `authorize-challenge` call (step 7). + /// Authorization code from the final `authorize-challenge` call. let code: String? - /// Access token from the `/token` exchange (step 8). + /// Access token from the `/token` exchange. let accessToken: String? /// Top-level `_links`, keyed by relation, value is the raw href string. diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index 3b00f91e28..ee0256b391 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -24,11 +24,11 @@ import Foundation -/// Validated outcome of an `authorize-challenge` call (steps 1 and 7). +/// Validated outcome of an `authorize-challenge` call. enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { - /// Bootstrap (step 1): `401` carrying the continuation token and the `reset_password` link. + /// Bootstrap: `401` carrying the continuation token and the entry links (`sign_up`/`sign_in`/`reset_password`). case continuationToken(continuationToken: String, links: [String: String]) - /// Completion (step 7): the authorization code to exchange for tokens. + /// Completion: the authorization code to exchange for tokens. case authorizationCode(code: String) case error(MSALNativeAuthFlowError) @@ -109,7 +109,7 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { } } -/// Validated outcome of the `/token` exchange (step 8). +/// Validated outcome of the `/token` exchange. enum MSALNativeAuthV2TokenValidatedResponse { case success(accessToken: String?) case error(MSALNativeAuthFlowError) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift index 526d600903..fe4f59064e 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift @@ -27,17 +27,10 @@ import Foundation /// Fixed (non server-driven) endpoints used by the Native Auth V2 flows. /// /// V2 is server-driven: most steps follow `_links` hrefs returned by the server. Only -/// the bootstrap / terminal OAuth endpoints and the SSPR entry endpoint have fixed paths -/// the client must know up-front. +/// the bootstrap / token exchange have fixed paths the client must know up-front. enum MSALNativeAuthV2Endpoint: String, CaseIterable { - /// Bootstrap (step 1) and completion (step 7). + /// Bootstrap case authorizeChallenge = "/oauth2/v2.0/authorize-challenge" - /// Token exchange (step 8). + /// Token exchange case token = "/oauth2/v2.0/token" - /// Self-service password reset entry (step 2). - case resetPasswordStart = "/api/v0.1/auth/resetpassword" - /// Sign in entry (used when the bootstrap response omits a `sign_in` link). - case signInStart = "/api/v0.1/signin/start" - /// Sign up entry (used when the bootstrap response omits a `sign_up` link). - case signUpStart = "/api/v0.1/signup/start" } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 27f7c79d40..6f1a35360d 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -26,55 +26,96 @@ import Foundation @_implementationOnly import MSAL_Private -/// Builds the `MSIDHttpRequest` objects for each step of the Native Auth V2 flows. protocol MSALNativeAuthV2RequestProviding { - /// Step 1: bootstrap `authorize-challenge` (no continuation token) → `401` + continuation token. - func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest - - /// Step 7: completion `authorize-challenge` (with continuation token) → authorization code. - func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest - - /// Step 8: token exchange. - func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest - - /// Step 2: SSPR entry (fixed endpoint). - func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Sign up entry: posts the username to the bootstrap `sign_up` href. + func signUpStart(username: String, + continuationToken: String, + href: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest - /// Sign in entry: posts the username to the bootstrap `sign_in` href (or the fixed endpoint). - func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Sign in entry: posts the username to the bootstrap `sign_in` href. + func signInStart(username: String, + continuationToken: String, + href: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest - /// Sign up entry: posts the username to the bootstrap `sign_up` href (or the fixed endpoint). - func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// SSPR entry, posted to the bootstrap `reset_password` href. + func resetPasswordStart(username: String, + continuationToken: String, + href: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest /// Submit a password to a server `verify` href (sign in / MFA primary factor). - func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + func submitPassword(href: String, + password: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest /// Submit a one-time `code` to a server `verify` / `activate` href (sign in / sign up / MFA / JIT). - func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + func submitCode(href: String, + code: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest /// Submit collected attributes (sign up) to a server `submitAttributes` href. - func submitAttributes( - href: String, - attributes: [String: Any], - continuationToken: String, - context: MSALNativeAuthRequestContext + func submitAttributes(href: String, + attributes: [String: Any], + continuationToken: String, + context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest /// Register a strong-auth method (JIT) by posting the target to a server `enroll` href. - func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + func registerMethod(href: String, + target: String?, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest - /// Step 3: send EOTP (server `challenge` / `resend` href). - func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Send EOTP (server `challenge` / `resend` href). + func challenge(href: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest - /// Step 4: verify OTP (server `verify` href). - func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Verify OTP (server `verify` href). + func verify(href: String, + otp: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest - /// Step 5: update password (server `update` href, PUT). - func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Update password (server `update` href, PUT). + func updatePassword(href: String, + newPassword: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest + + /// Poll for completion (server `poll` href). + func poll(href: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest + + /// Start `authorize-challenge` (no continuation token) → `401` + continuation token. + func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + + /// Continue `authorize-challenge` (with continuation token) → authorization code. + func authorizeChallengeContinue(continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest - /// Step 6: poll for completion (server `poll` href). - func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + /// Token exchange. + func token(code: String, + scopes: [String], + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest } final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { @@ -87,57 +128,29 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { self.configurator = MSALNativeAuthV2RequestConfigurator(config: config) } - func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - // The bootstrap authorize-challenge sends ONLY `client_id`. Including `scope` here makes - // ESTS mint a continuation token scoped for the authorization-code path, which the - // signup/signin/resetpassword `start` endpoints reject with AADSTS55200 ("continuation_token - // is invalid"). `scope` is supplied later on the `token` call instead (see `token(code:scopes:context:)`). - return try configurator.configure(parameters: MSALNativeAuthV2AuthorizeChallengeStartParameters(context: context, clientId: config.clientId)) - } - - func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - // The authorize-challenge-resume call sends ONLY `continuation_token` (no client_id/scope), - // matching the server contract; extra parameters cause AADSTS55200 / auth failures. - return try configurator.configure( - parameters: MSALNativeAuthV2AuthorizeChallengeContinueParameters(context: context, continuationToken: continuationToken) - ) - } - - func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - // The authorization_code exchange sends client_id, code, grant_type and the requested - // `scope` (caller scopes merged with the default OIDC scopes). `scope` is sent here — not - // on the authorize-challenge bootstrap — because including it in the bootstrap makes ESTS - // mint a continuation token the start endpoints reject with AADSTS55200. - // The `/token` endpoint returns a standard OAuth token response (NOT HAL), so this request - // yields the raw JSON dictionary for the controller to parse into an `MSIDTokenResponse` and - // persist to the cache. - // `client_info=true` (added by the parameter class) asks ESTS to return the `client_info` - // blob (uid/utid) in the token response. IdentityCore's AAD-v2/CIAM factory rejects any - // token response without it ("Client info was not returned in the server response"), which - // surfaces to the caller as "Unable to save tokens to the cache". - return try configurator.configure(parameters: MSALNativeAuthV2TokenParameters( - context: context, - clientId: config.clientId, - code: code, - scopes: scopes - )) - } - - func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func signUpStart(username: String, + continuationToken: String, + href: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( context: context, - target: .endpoint(.resetPasswordStart), - apiId: .telemetryApiIdV2ResetPassword, - operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, + target: .href(href), + apiId: .telemetryApiIdV2SignUp, + operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, username: username, continuationToken: continuationToken )) } - func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func signInStart(username: String, + continuationToken: String, + href: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( context: context, - target: href.map { .href($0) } ?? .endpoint(.signInStart), + target: .href(href), apiId: .telemetryApiIdV2SignIn, operationType: MSALNativeAuthV2OperationType.signInStart.rawValue, username: username, @@ -145,18 +158,26 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func resetPasswordStart(username: String, + continuationToken: String, + href: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( context: context, - target: href.map { .href($0) } ?? .endpoint(.signUpStart), - apiId: .telemetryApiIdV2SignUp, - operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, + target: .href(href), + apiId: .telemetryApiIdV2ResetPassword, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, username: username, continuationToken: continuationToken )) } - func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func submitPassword(href: String, + password: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, @@ -167,7 +188,11 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func submitCode(href: String, + code: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, @@ -178,11 +203,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func submitAttributes( - href: String, - attributes: [String: Any], - continuationToken: String, - context: MSALNativeAuthRequestContext + func submitAttributes(href: String, + attributes: [String: Any], + continuationToken: String, + context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, @@ -194,7 +218,11 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func registerMethod(href: String, + target: String?, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { var body: [AnyHashable: Any] = ["continuationToken": continuationToken] if let target = target { body["target"] = target @@ -209,7 +237,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func challenge(href: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, @@ -220,7 +251,11 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func verify(href: String, + otp: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, @@ -231,11 +266,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func updatePassword( - href: String, - newPassword: String, - continuationToken: String, - context: MSALNativeAuthRequestContext + func updatePassword(href: String, + newPassword: String, + continuationToken: String, + context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, @@ -247,7 +281,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func poll(href: String, + continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, @@ -257,4 +294,28 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { body: ["continuationToken": continuationToken] )) } + + func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + return try configurator.configure(parameters: MSALNativeAuthV2AuthorizeChallengeStartParameters(context: context, clientId: config.clientId)) + } + + func authorizeChallengeContinue(continuationToken: String, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + return try configurator.configure( + parameters: MSALNativeAuthV2AuthorizeChallengeContinueParameters(context: context, continuationToken: continuationToken) + ) + } + + func token(code: String, + scopes: [String], + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + return try configurator.configure(parameters: MSALNativeAuthV2TokenParameters( + context: context, + clientId: config.clientId, + code: code, + scopes: scopes + )) + } } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index 260d515eaa..f9b9ba1bac 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -110,7 +110,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_whenUserNotFound_returnsError() async { requestProviderMock.mockRequest() validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-bootstrap", links: [:]) + .continuationToken(continuationToken: "ct-bootstrap", links: ["reset_password": "https://contoso.com/reset"]) ] validatorMock.interactionResponses = [ .error(MSALNativeAuthFlowError(kind: .userNotFound)) diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index 4bc04aad6e..611d5aa9e6 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -84,17 +84,17 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { return try resolveRequest() } - func resetPasswordStart(username: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func resetPasswordStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { resetPasswordStartCalled = true return try resolveRequest() } - func signInStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func signInStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { signInStartCalled = true return try resolveRequest() } - func signUpStart(username: String, continuationToken: String, href: String?, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func signUpStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { signUpStartCalled = true return try resolveRequest() } From 77810a086305e5399e6a3349fcf5ff62652bb8f3 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 10 Jul 2026 16:17:09 +0100 Subject: [PATCH 15/85] decode response and fail rther than failing on next request --- .../v2/MSALNativeAuthV2FlowController.swift | 30 +------------ .../MSALNativeAuthV2ResponseValidator.swift | 43 ++++++++++++++++--- .../MSALNativeAuthV2ValidatedResponses.swift | 18 ++++---- ...ALNativeAuthV2ResponseValidatorTests.swift | 24 +++++++++++ 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 06e56868f2..ad1a790beb 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -155,16 +155,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa token2 = token challengeHref = href case .challengeRequired(let token, let href, _): - guard let href = href else { - return await mapInteraction( - startResult, - flowType: .signIn, - username: parameters.username, - scopes: scopes, - event: event, - context: context - ) - } token2 = token challengeHref = href default: @@ -185,7 +175,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // When a password was supplied and the password factor is required, submit it now // so password sign-in completes in one call. - if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult, let verifyHref = verifyHref { + if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult { let verifyResult = await performInteraction(context: context) { try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) } @@ -238,14 +228,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } // Auto-trigger the challenge (send EOTP). - guard let challengeHref = challengeHref else { - return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing challenge link")), - event: event, - context: context - ) - } - let challengeResult = await performInteraction(context: context) { try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) } @@ -344,7 +326,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // swiftlint:disable:next cyclomatic_complexity function_body_length + // swiftlint:disable:next function_body_length func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmit, context: context) @@ -372,14 +354,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return interactionFailure(updateResult, event: event, context: context, newState: nil) } - guard let pollHref = pollHref else { - return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing poll link")), - event: event, - context: context - ) - } - // Poll until the operation completes. var completionToken: String? for attempt in 0.. 0 || response.hint != nil || response.methodType == "email" { return .codeRequired( @@ -114,33 +119,48 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin } return .passwordRequired(continuationToken: continuationToken, verifyHref: verifyHref) case "enroll", "register": + guard let enrollHref = response.href(forRelation: "enroll") ?? response.href(forRelation: "register") else { + return Self.missingLink("enroll") + } return .registrationRequired( continuationToken: continuationToken, - enrollHref: response.href(forRelation: "enroll") ?? response.href(forRelation: "register"), + enrollHref: enrollHref, methods: response.methods ) case "activate": + guard let activateHref = response.href(forRelation: "activate") else { + return Self.missingLink("activate") + } return .activationRequired( continuationToken: continuationToken, - activateHref: response.href(forRelation: "activate"), + activateHref: activateHref, sentTo: response.hint ?? "", codeLength: response.codeLength ?? 0 ) case "collectAttributes": + guard let submitHref = response.href(forRelation: "submitAttributes") ?? response.href(forRelation: "submitattributes") else { + return Self.missingLink("submitAttributes") + } return .attributesRequired( continuationToken: continuationToken, attributes: response.attributes, - submitHref: response.href(forRelation: "submitAttributes") ?? response.href(forRelation: "submitattributes") + submitHref: submitHref ) case "update": + guard let updateHref = response.href(forRelation: "update") ?? response.href(forRelation: "self") else { + return Self.missingLink("update") + } return .updateRequired( continuationToken: continuationToken, - updateHref: response.href(forRelation: "update") ?? response.href(forRelation: "self") + updateHref: updateHref ) case "poll": + guard let pollHref = response.href(forRelation: "poll") else { + return Self.missingLink("poll") + } return .pollInProgress( continuationToken: continuationToken, - pollHref: response.href(forRelation: "poll") + pollHref: pollHref ) default: return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) @@ -164,6 +184,15 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin // MARK: - Error mapping + /// The server returned an action that requires a follow-up link, but that link is absent. + /// Fail here rather than passing a missing href down to the next request. + private static func missingLink(_ relation: String) -> MSALNativeAuthV2InteractionValidatedResponse { + return .error(MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Invalid interaction response: missing '\(relation)' link" + )) + } + private static func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { let message = serverError.message let errorCodes = Self.estsErrorCodes(from: message) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index ee0256b391..db66d73116 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -55,23 +55,23 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { /// available authentication methods (each with its own `challenge` link). case signInMethods(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) /// `action == challenge`: a verification method is available; the SDK should auto-trigger the challenge. - case challengeRequired(continuationToken: String, challengeHref: String?, hint: String?) + case challengeRequired(continuationToken: String, challengeHref: String, hint: String?) /// `action == verify` on a password method: the user must enter their password. - case passwordRequired(continuationToken: String, verifyHref: String?) + case passwordRequired(continuationToken: String, verifyHref: String) /// `action == verify`: a one-time code is required from the user. - case codeRequired(continuationToken: String, verifyHref: String?, resendHref: String?, sentTo: String, codeLength: Int) + case codeRequired(continuationToken: String, verifyHref: String, resendHref: String?, sentTo: String, codeLength: Int) /// `action == verify` after a password, carrying a `challenge` link and the MFA methods. - case mfaRequired(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod], challengeHref: String?) + case mfaRequired(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod], challengeHref: String) /// `action == enroll`/`register`: strong-auth (JIT) registration is required; pick a method to enroll. - case registrationRequired(continuationToken: String, enrollHref: String?, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) + case registrationRequired(continuationToken: String, enrollHref: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) /// `action == activate`: a JIT enrollment code is required from the user. - case activationRequired(continuationToken: String, activateHref: String?, sentTo: String, codeLength: Int) + case activationRequired(continuationToken: String, activateHref: String, sentTo: String, codeLength: Int) /// `action == collectAttributes`: sign-up attributes are required from the user. - case attributesRequired(continuationToken: String, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry], submitHref: String?) + case attributesRequired(continuationToken: String, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry], submitHref: String) /// `action == update`: a new password is required from the user. - case updateRequired(continuationToken: String, updateHref: String?) + case updateRequired(continuationToken: String, updateHref: String) /// `action == poll`: the operation is still running; keep polling. - case pollInProgress(continuationToken: String, pollHref: String?) + case pollInProgress(continuationToken: String, pollHref: String) /// `state == continue`: the flow is ready to complete (call `authorize-challenge`). case readyToComplete(continuationToken: String) case error(MSALNativeAuthFlowError) diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index b73312d4e4..ffdc81f999 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -133,6 +133,30 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { XCTAssertEqual(result, .pollInProgress(continuationToken: "ct", pollHref: "https://contoso.com/poll")) } + func test_validateInteraction_updateAction_withoutUpdateLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct") + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + } + + func test_validateInteraction_pollAction_withoutPollLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct") + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + } + + func test_validateInteraction_verifyAction_withoutVerifyLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "verify", continuationToken: "ct", codeLength: 8, hint: "u***@contoso.com") + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + } + + func test_validateInteraction_collectAttributesAction_withoutSubmitLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "collectAttributes", continuationToken: "ct") + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + } + func test_validateInteraction_continueState_returnsReadyToComplete() { let response = makeResponse(state: "continue", continuationToken: "ct") let result = sut.validateInteraction(.success(response)) From 78e2ac8448d93aba59322f3dcfdaf854ae3dc019 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 10:23:40 +0100 Subject: [PATCH 16/85] Removed static and Self. calls --- .../v2/MSALNativeAuthV2FlowController.swift | 4 +-- ...SALNativeAuthV2HALResponseSerializer.swift | 26 +++++++------- .../MSALNativeAuthV2ResponseValidator.swift | 36 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index ad1a790beb..bc0495ad95 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -768,7 +768,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch tokenResponseResult { case .success(let tokenResponse): do { - let msidConfiguration = resultFactory.makeMSIDConfiguration(scopes: Self.scopes(from: tokenResponse)) + let msidConfiguration = resultFactory.makeMSIDConfiguration(scopes: retrieveScopes(from: tokenResponse)) let tokenResult = try cacheTokenResponse(tokenResponse, context: context, msidConfiguration: msidConfiguration) guard let accountResult = resultFactory.makeUserAccountResult(tokenResult: tokenResult, context: context) else { @@ -857,7 +857,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } /// Extracts the granted scopes from a token response so the cache target matches what was issued. - private static func scopes(from tokenResponse: MSIDTokenResponse) -> [String] { + private func retrieveScopes(from tokenResponse: MSIDTokenResponse) -> [String] { guard let scope = tokenResponse.scope, !scope.isEmpty else { return [] } diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index d3006a5569..6e1adbe081 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -34,6 +34,11 @@ import Foundation /// `HALResource`. final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSerialization { + /// Known link relations the server may return as flat, top-level string fields rather than + /// nested under HAL `_links` (e.g. the `authorize-challenge` bootstrap returns + /// `reset_password` / `sign_in` / `sign_up` at the top level). + private let topLevelLinkRelations = ["reset_password", "sign_in", "sign_up", "signin", "signup"] + func responseObject(for httpResponse: HTTPURLResponse?, data: Data?, context: MSIDRequestContext?) throws -> Any { let statusCode = httpResponse?.statusCode ?? 0 let correlationId = MSALNativeAuthHALResponse.retrieveCorrelationIdFromHeaders(from: httpResponse) @@ -76,21 +81,16 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali hint: resource.string(forKey: "hint"), methodId: resource.string(forKey: "id"), methodType: resource.string(forKey: "type"), - attributes: Self.parseAttributes(from: json), + attributes: parseAttributes(from: json), code: resource.string(forKey: "code"), accessToken: resource.string(forKey: "access_token"), - links: Self.parseLinks(from: resource, json: json), - methods: Self.parseMethods(from: resource), - error: Self.parseError(from: json, fallbackCorrelationId: correlationId) + links: parseLinks(from: resource, json: json), + methods: parseMethods(from: resource), + error: parseError(from: json, fallbackCorrelationId: correlationId) ) } - /// Known link relations the server may return as flat, top-level string fields rather than - /// nested under HAL `_links` (e.g. the `authorize-challenge` bootstrap returns - /// `reset_password` / `sign_in` / `sign_up` at the top level). - private static let topLevelLinkRelations = ["reset_password", "sign_in", "sign_up", "signin", "signup"] - - private static func parseLinks(from resource: HALResource, json: [String: Any]) -> [String: String] { + private func parseLinks(from resource: HALResource, json: [String: Any]) -> [String: String] { var result: [String: String] = [:] for (relation, links) in resource.links { if let href = links.first?.href { @@ -106,7 +106,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali return result } - private static func parseMethods(from resource: HALResource) -> [MSALNativeAuthHALResponse.EmbeddedMethod] { + private func parseMethods(from resource: HALResource) -> [MSALNativeAuthHALResponse.EmbeddedMethod] { let methodResources = resource.embeddedResources(rel: "methods") return methodResources.map { dict in let methodResource = HALResource(json: dict) @@ -125,7 +125,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali } } - private static func parseAttributes(from json: [String: Any]) -> [MSALNativeAuthHALResponse.RequiredAttributeEntry] { + private func parseAttributes(from json: [String: Any]) -> [MSALNativeAuthHALResponse.RequiredAttributeEntry] { guard let rawAttributes = json["attributes"] as? [[String: Any]] else { return [] } @@ -139,7 +139,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali } } - private static func parseError(from json: [String: Any], fallbackCorrelationId: UUID?) -> MSALNativeAuthHALResponse.ServerError? { + private func parseError(from json: [String: Any], fallbackCorrelationId: UUID?) -> MSALNativeAuthHALResponse.ServerError? { guard let errorDict = json["error"] as? [String: Any] else { return nil } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index ce94fbf829..67263323aa 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -38,10 +38,10 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { switch result { case .failure(let error): - return .error(Self.flowError(from: error)) + return .error(flowError(from: error)) case .success(let response): if let error = response.error { - return .error(Self.flowError(from: error)) + return .error(flowError(from: error)) } if let code = response.code { return .authorizationCode(code: code) @@ -62,10 +62,10 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) -> MSALNativeAuthV2InteractionValidatedResponse { switch result { case .failure(let error): - return .error(Self.flowError(from: error)) + return .error(flowError(from: error)) case .success(let response): if let error = response.error { - return .error(Self.flowError(from: error)) + return .error(flowError(from: error)) } if response.state == "continue" { @@ -88,7 +88,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case "challenge": let method = response.methods.first guard let challengeHref = method?.links["challenge"] ?? response.href(forRelation: "challenge") else { - return Self.missingLink("challenge") + return missingLink("challenge") } return .challengeRequired( continuationToken: continuationToken, @@ -105,7 +105,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) } guard let verifyHref = response.href(forRelation: "verify") else { - return Self.missingLink("verify") + return missingLink("verify") } // An email/OOB method carries a hint and/or a code length; a password method does not. if (response.codeLength ?? 0) > 0 || response.hint != nil || response.methodType == "email" { @@ -120,7 +120,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .passwordRequired(continuationToken: continuationToken, verifyHref: verifyHref) case "enroll", "register": guard let enrollHref = response.href(forRelation: "enroll") ?? response.href(forRelation: "register") else { - return Self.missingLink("enroll") + return missingLink("enroll") } return .registrationRequired( continuationToken: continuationToken, @@ -129,7 +129,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case "activate": guard let activateHref = response.href(forRelation: "activate") else { - return Self.missingLink("activate") + return missingLink("activate") } return .activationRequired( continuationToken: continuationToken, @@ -139,7 +139,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case "collectAttributes": guard let submitHref = response.href(forRelation: "submitAttributes") ?? response.href(forRelation: "submitattributes") else { - return Self.missingLink("submitAttributes") + return missingLink("submitAttributes") } return .attributesRequired( continuationToken: continuationToken, @@ -148,7 +148,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case "update": guard let updateHref = response.href(forRelation: "update") ?? response.href(forRelation: "self") else { - return Self.missingLink("update") + return missingLink("update") } return .updateRequired( continuationToken: continuationToken, @@ -156,7 +156,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case "poll": guard let pollHref = response.href(forRelation: "poll") else { - return Self.missingLink("poll") + return missingLink("poll") } return .pollInProgress( continuationToken: continuationToken, @@ -173,10 +173,10 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) -> MSALNativeAuthV2TokenValidatedResponse { switch result { case .failure(let error): - return .error(Self.flowError(from: error)) + return .error(flowError(from: error)) case .success(let response): if let error = response.error { - return .error(Self.flowError(from: error)) + return .error(flowError(from: error)) } return .success(accessToken: response.accessToken) } @@ -186,16 +186,16 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin /// The server returned an action that requires a follow-up link, but that link is absent. /// Fail here rather than passing a missing href down to the next request. - private static func missingLink(_ relation: String) -> MSALNativeAuthV2InteractionValidatedResponse { + private func missingLink(_ relation: String) -> MSALNativeAuthV2InteractionValidatedResponse { return .error(MSALNativeAuthFlowError( kind: .generalError, errorDescription: "Invalid interaction response: missing '\(relation)' link" )) } - private static func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { + private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { let message = serverError.message - let errorCodes = Self.estsErrorCodes(from: message) + let errorCodes = estsErrorCodes(from: message) let kind: MSALNativeAuthFlowError.Kind if serverError.innerErrorCode == "invalidContinuationToken" { @@ -225,7 +225,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin /// Extracts the numeric ESTS error codes (e.g. `50126` from `AADSTS50126`) embedded in a /// server error message. - private static func estsErrorCodes(from message: String?) -> [Int] { + private func estsErrorCodes(from message: String?) -> [Int] { guard let message = message else { return [] } @@ -246,7 +246,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return codes } - private static func flowError(from error: Error) -> MSALNativeAuthFlowError { + private func flowError(from error: Error) -> MSALNativeAuthFlowError { if let flowError = error as? MSALNativeAuthFlowError { return flowError } From ce51babd8de2f005cdb2444fe74f6c74ac3d8938 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 11:28:49 +0100 Subject: [PATCH 17/85] Fail fast on missing authorize-challenge link; thread flowType into V2 validator Move required-link validation into the V2 response validator so a flow fails as soon as the server response is missing the link the next step needs, instead of failing later on a subsequent request. - Thread MSALNativeAuthV2FlowType from each entry point (signUp/signIn/ resetPassword) down to validateAuthorizeChallenge. Each flow now declares its flowType exactly once; downstream steps derive it from the flow type or the continuation state. - Resolve the single flow-specific entry link in the validator: .continuationToken now carries a non-optional href instead of a links dictionary, and a missing link returns a descriptive .error immediately. - Make interaction-response hrefs non-optional and guard each required link in validateInteraction, so the controller can no longer receive a nil href. - Expose MSALNativeAuthV2FlowType.link (CaseIterable) as the single source of the sign_up/sign_in/reset_password relations; drop the duplicated serializer list. - Remove dead concatenated link spellings (signup/signin/resetpassword/ submitattributes); sign_up/sign_in/reset_password are the only valid relations. - Rename the authorize-challenge step off "bootstrap" for clarity. - Update the validator mock and add fail-fast regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v2/MSALNativeAuthV2FlowController.swift | 92 ++++++++++--------- .../v2/MSALNativeAuthV2FlowResult.swift | 14 ++- .../v2/MSALNativeAuthHALResponse.swift | 2 +- ...SALNativeAuthV2HALResponseSerializer.swift | 13 +-- ...MSALNativeAuthV2ResponseErrorHandler.swift | 2 +- .../MSALNativeAuthV2ResponseValidator.swift | 19 +++- .../MSALNativeAuthV2ValidatedResponses.swift | 9 +- .../network/v2/MSALNativeAuthV2Endpoint.swift | 4 +- .../v2/MSALNativeAuthV2HrefURLResolver.swift | 4 +- .../MSALNativeAuthV2RequestConfigurator.swift | 2 +- .../v2/MSALNativeAuthV2RequestProvider.swift | 6 +- .../MSALNativeAuthV2FlowControllerTests.swift | 12 +-- ...SALNativeAuthV2ResponseValidatorMock.swift | 5 +- ...ALNativeAuthV2ResponseValidatorTests.swift | 19 +++- 14 files changed, 118 insertions(+), 85 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index bc0495ad95..8abda841ad 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -67,17 +67,15 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Entry points func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { + let flowType: MSALNativeAuthV2FlowType = .signUp let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUp, context: context) let scopes = joinScopes(parameters.scopes) - // Bootstrap authorize-challenge (expects 401 + continuation token + sign_up link). - let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let continuationToken, let links) = bootstrap else { - return failure(bootstrap, event: event, context: context) - } - guard let signUpLink = links["sign_up"] ?? links["signup"] else { - return missingLinkFailure(relation: "sign_up", event: event, context: context) + // Authorization challenge (expects 401 + continuation token + sign_up link). + let authorizationChallenge = await performAuthorizeChallengeStart(flowType: flowType, context: context) + guard case .continuationToken(let continuationToken, let signUpLink) = authorizationChallenge else { + return failure(authorizationChallenge, event: event, context: context) } // Sign-up start (auto-triggers the email challenge). @@ -90,11 +88,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - return await mapInteraction(startResult, flowType: .signUp, username: parameters.username, scopes: scopes, event: event, context: context) + return await mapInteraction(startResult, flowType: flowType, username: parameters.username, scopes: scopes, event: event, context: context) } // swiftlint:disable:next function_body_length func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { + let flowType: MSALNativeAuthV2FlowType = .signIn let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil ? .telemetryApiIdSignInWithPasswordStart @@ -102,13 +101,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let event = makeAndStartTelemetryEvent(id: apiId, context: context) let scopes = joinScopes(parameters.scopes) - // Bootstrap authorize-challenge (expects 401 + continuation token + sign_in link). - let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let continuationToken, let links) = bootstrap else { - return failure(bootstrap, event: event, context: context) - } - guard let signInLink = links["sign_in"] ?? links["signin"] else { - return missingLinkFailure(relation: "sign_in", event: event, context: context) + // Authorization challenge (expects 401 + continuation token + sign_in link). + let authorizationChallenge = await performAuthorizeChallengeStart(flowType: flowType, context: context) + guard case .continuationToken(let continuationToken, let signInLink) = authorizationChallenge else { + return failure(authorizationChallenge, event: event, context: context) } // Sign-in (method discovery). @@ -160,7 +156,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa default: return await mapInteraction( startResult, - flowType: .signIn, + flowType: flowType, username: parameters.username, scopes: scopes, event: event, @@ -181,7 +177,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( verifyResult, - flowType: .signIn, + flowType: flowType, username: parameters.username, scopes: scopes, event: event, @@ -191,7 +187,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return await mapInteraction( challengeResult, - flowType: .signIn, + flowType: flowType, username: parameters.username, scopes: scopes, event: event, @@ -200,17 +196,15 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { + let flowType: MSALNativeAuthV2FlowType = .resetPassword let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) let scopes = joinScopes(parameters.scopes) - // Bootstrap authorize-challenge (expects 401 + continuation token + reset_password link). - let bootstrap = await performAuthorizeChallengeStart(context: context) - guard case .continuationToken(let continuationToken, let links) = bootstrap else { - return failure(bootstrap, event: event, context: context) - } - guard let resetPasswordLink = links["reset_password"] ?? links["resetpassword"] else { - return missingLinkFailure(relation: "reset_password", event: event, context: context) + // Authorization challenge (expects 401 + continuation token + reset_password link). + let authorizationChallenge = await performAuthorizeChallengeStart(flowType: flowType, context: context) + guard case .continuationToken(let continuationToken, let resetPasswordLink) = authorizationChallenge else { + return failure(authorizationChallenge, event: event, context: context) } // Reset-password start. @@ -232,7 +226,15 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) } - return handleCodeRequired(challengeResult, username: parameters.username, fallbackHint: hint, scopes: scopes, event: event, context: context) + return handleCodeRequired( + challengeResult, + flowType: flowType, + username: parameters.username, + fallbackHint: hint, + scopes: scopes, + event: event, + context: context + ) } // MARK: - Continuation @@ -277,7 +279,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch result { case .updateRequired(let token, let updateHref): let newState = makeState( - .resetPassword, + continuation.flowType, continuationToken: token, links: ["update": updateHref], username: continuation.username, @@ -396,6 +398,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await completeWithToken( + flowType: continuation.flowType, continuationToken: completionToken, username: continuation.username, scopes: continuation.scopes, @@ -566,6 +569,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return handleCodeRequired( result, + flowType: continuation.flowType, username: continuation.username, fallbackHint: continuation.sentToHint, scopes: continuation.scopes, @@ -577,22 +581,24 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Shared step helpers private func performAuthorizeChallengeStart( + flowType: MSALNativeAuthV2FlowType, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { let result: Result = await send { try self.requestProvider.authorizeChallengeStart(context: context) } - return responseValidator.validateAuthorizeChallenge(result) + return responseValidator.validateAuthorizeChallenge(result, flowType: flowType) } private func performAuthorizeChallengeContinue( + flowType: MSALNativeAuthV2FlowType, continuationToken: String, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { let result: Result = await send { try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, context: context) } - return responseValidator.validateAuthorizeChallenge(result) + return responseValidator.validateAuthorizeChallenge(result, flowType: flowType) } private func performInteraction( @@ -620,6 +626,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa private func handleCodeRequired( _ result: MSALNativeAuthV2InteractionValidatedResponse, + flowType: MSALNativeAuthV2FlowType, username: String?, fallbackHint: String?, scopes: [String], @@ -629,7 +636,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch result { case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( - .resetPassword, + flowType, continuationToken: token, links: ["verify": verifyHref, "resend": resendHref], username: username, @@ -666,7 +673,14 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) async -> MSALNativeAuthV2FlowControllerResponse { switch result { case .readyToComplete(let token): - return await completeWithToken(continuationToken: token, username: username, scopes: scopes, event: event, context: context) + return await completeWithToken( + flowType: flowType, + continuationToken: token, + username: username, + scopes: scopes, + event: event, + context: context + ) case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( flowType, @@ -753,13 +767,14 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa /// The `/token` response is persisted to the shared MSAL token cache so the returned /// ``MSALNativeAuthUserAccountResult`` can retrieve access tokens via `getAccessToken(...)`. private func completeWithToken( + flowType: MSALNativeAuthV2FlowType, continuationToken: String, username: String?, scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2FlowControllerResponse { - let codeResult = await performAuthorizeChallengeContinue(continuationToken: continuationToken, context: context) + let codeResult = await performAuthorizeChallengeContinue(flowType: flowType, continuationToken: continuationToken, context: context) guard case .authorizationCode(let code) = codeResult else { return failure(codeResult, event: event, context: context) } @@ -972,19 +987,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return response(.error(error: error, newState: nil), context: context) } - private func missingLinkFailure( - relation: String, - event: MSIDTelemetryAPIEvent?, - context: MSALNativeAuthRequestContext - ) -> MSALNativeAuthV2FlowControllerResponse { - let error = MSALNativeAuthFlowError( - kind: .generalError, - errorDescription: "Invalid authorize-challenge response: missing '\(relation)' link" - ) - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) - } - private func interactionFailure( _ validated: MSALNativeAuthV2InteractionValidatedResponse, event: MSIDTelemetryAPIEvent?, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift index 4fc094dcfd..8955375456 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -25,10 +25,22 @@ import Foundation /// Identifies which V2 flow a ``MSALNativeAuthFlowState`` belongs to. -enum MSALNativeAuthV2FlowType { +enum MSALNativeAuthV2FlowType: CaseIterable { case signUp case signIn case resetPassword + + /// The `authorize-challenge` link relation this flow follows. + var link: String { + switch self { + case .signUp: + return "sign_up" + case .signIn: + return "sign_in" + case .resetPassword: + return "reset_password" + } + } } /// Internal continuation context carried by a ``MSALNativeAuthFlowState``. diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index da9b2924f5..1995f371f2 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -28,7 +28,7 @@ import Foundation /// Generic, server-driven HAL response used by all Native Auth V2 flows. /// -/// Every V2 HTTP outcome (200 success, the bootstrap `401` from `authorize-challenge`, +/// Every V2 HTTP outcome (200 success, the `401` from `authorize-challenge`, /// and `4xx` error bodies) is parsed into a single ``MSALNativeAuthHALResponse``. The /// V2 response validator then inspects `error`, `state` and `action` to decide how the /// flow should proceed. HAL parsing itself is delegated to the shared diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 6e1adbe081..8caaca0636 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -34,11 +34,6 @@ import Foundation /// `HALResource`. final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSerialization { - /// Known link relations the server may return as flat, top-level string fields rather than - /// nested under HAL `_links` (e.g. the `authorize-challenge` bootstrap returns - /// `reset_password` / `sign_in` / `sign_up` at the top level). - private let topLevelLinkRelations = ["reset_password", "sign_in", "sign_up", "signin", "signup"] - func responseObject(for httpResponse: HTTPURLResponse?, data: Data?, context: MSIDRequestContext?) throws -> Any { let statusCode = httpResponse?.statusCode ?? 0 let correlationId = MSALNativeAuthHALResponse.retrieveCorrelationIdFromHeaders(from: httpResponse) @@ -97,10 +92,10 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali result[relation] = href } } - // Merge flat, top-level link relations (HAL `_links` takes precedence if both are present). - for relation in topLevelLinkRelations where result[relation] == nil { - if let href = json[relation] as? String { - result[relation] = href + + for flowType in MSALNativeAuthV2FlowType.allCases where result[flowType.link] == nil { + if let href = json[flowType.link] as? String { + result[flowType.link] = href } } return result diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift index 5f3006b9e2..7f5fc79977 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift @@ -30,7 +30,7 @@ import Foundation /// /// `MSIDHttpRequest` only routes HTTP 200 through the response serializer; every other /// status code is delivered here. In V2 the meaningful body lives on every outcome — -/// the bootstrap `401` from `authorize-challenge` carries the `continuation_token`, and +/// the `401` from `authorize-challenge` carries the `continuation_token`, and /// `4xx` responses carry an `error` object. So this handler simply re-runs the HAL /// response serializer for any status and hands the parsed ``MSALNativeAuthHALResponse`` /// back to the caller; the V2 validator (not the transport) decides success vs failure. diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 67263323aa..062dbaf052 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -26,7 +26,10 @@ import Foundation /// Maps a raw ``MSALNativeAuthHALResponse`` (or transport error) into a validated, controller-facing response. protocol MSALNativeAuthV2ResponseValidating { - func validateAuthorizeChallenge(_ result: Result) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse + func validateAuthorizeChallenge( + _ result: Result, + flowType: MSALNativeAuthV2FlowType + ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse } @@ -34,7 +37,8 @@ protocol MSALNativeAuthV2ResponseValidating { final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidating { func validateAuthorizeChallenge( - _ result: Result + _ result: Result, + flowType: MSALNativeAuthV2FlowType ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { switch result { case .failure(let error): @@ -47,7 +51,14 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .authorizationCode(code: code) } if let continuationToken = response.continuationToken { - return .continuationToken(continuationToken: continuationToken, links: response.links) + let relation = flowType.link + guard let href = response.links[relation] else { + return .error(MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Invalid authorize-challenge response: missing '\(relation)' link" + )) + } + return .continuationToken(continuationToken: continuationToken, href: href) } return .error(MSALNativeAuthFlowError( kind: .generalError, @@ -138,7 +149,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin codeLength: response.codeLength ?? 0 ) case "collectAttributes": - guard let submitHref = response.href(forRelation: "submitAttributes") ?? response.href(forRelation: "submitattributes") else { + guard let submitHref = response.href(forRelation: "submitAttributes") else { return missingLink("submitAttributes") } return .attributesRequired( diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index db66d73116..fc1b66461b 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -26,16 +26,17 @@ import Foundation /// Validated outcome of an `authorize-challenge` call. enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { - /// Bootstrap: `401` carrying the continuation token and the entry links (`sign_up`/`sign_in`/`reset_password`). - case continuationToken(continuationToken: String, links: [String: String]) + /// `401` carrying the continuation token and the resolved entry link for the flow + /// (`sign_up` / `sign_in` / `reset_password`). + case continuationToken(continuationToken: String, href: String) /// Completion: the authorization code to exchange for tokens. case authorizationCode(code: String) case error(MSALNativeAuthFlowError) static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { - case let (.continuationToken(lToken, lLinks), .continuationToken(rToken, rLinks)): - return lToken == rToken && lLinks == rLinks + case let (.continuationToken(lToken, lHref), .continuationToken(rToken, rHref)): + return lToken == rToken && lHref == rHref case let (.authorizationCode(lCode), .authorizationCode(rCode)): return lCode == rCode case let (.error(lError), .error(rError)): diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift index fe4f59064e..a834d915d0 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift @@ -27,9 +27,9 @@ import Foundation /// Fixed (non server-driven) endpoints used by the Native Auth V2 flows. /// /// V2 is server-driven: most steps follow `_links` hrefs returned by the server. Only -/// the bootstrap / token exchange have fixed paths the client must know up-front. +/// the authorization challenge / token exchange have fixed paths the client must know up-front. enum MSALNativeAuthV2Endpoint: String, CaseIterable { - /// Bootstrap + /// Authorization challenge case authorizeChallenge = "/oauth2/v2.0/authorize-challenge" /// Token exchange case token = "/oauth2/v2.0/token" diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index f25ec8beb6..27922abd18 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -58,7 +58,7 @@ struct MSALNativeAuthV2HrefURLResolver { /// Resolves a server-provided `_links` href into an absolute URL against the authority host. /// /// The server returns hrefs whose leading path segment is a tenant identifier — typically the - /// tenant **GUID** (e.g. `/4710d5e4-.../api/v0.1/signup/start`). However, the bootstrap + /// tenant **GUID** (e.g. `/{tenantId}/api/v0.1/signup/start`). However, the authorization challenge /// continuation_token is bound to the tenant form used by the authority /// (`.onmicrosoft.com`); calling the GUID path makes ESTS reject the token with /// AADSTS55200 ("continuation_token is invalid"). To keep the tenant identifier consistent for @@ -87,7 +87,7 @@ struct MSALNativeAuthV2HrefURLResolver { } // Drop the href's leading tenant segment and reproduce the path against the authority's - // tenant path so the tenant identifier stays consistent with the bootstrap. + // tenant path so the tenant identifier stays consistent with the authorization challenge. components.path = authorityTenantPath + apiPath(from: hrefComponents.path) components.percentEncodedQuery = hrefComponents.percentEncodedQuery return try applyingDataCenter(to: components) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift index 3ceb34b136..fd12622b54 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -65,7 +65,7 @@ enum MSALNativeAuthV2RequestTarget { } } -/// `POST /authorize/challenge` bootstrap. Sends ONLY `client_id` (form encoded). +/// `POST /authorize/challenge` (the authorization challenge that starts a flow). Sends ONLY `client_id` (form encoded). struct MSALNativeAuthV2AuthorizeChallengeStartParameters: MSALNativeAuthV2Requestable { let context: MSALNativeAuthRequestContext let clientId: String diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 6f1a35360d..8071108ace 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -28,21 +28,21 @@ import Foundation protocol MSALNativeAuthV2RequestProviding { - /// Sign up entry: posts the username to the bootstrap `sign_up` href. + /// Sign up entry: posts the username to the authorize-challenge `sign_up` href. func signUpStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest - /// Sign in entry: posts the username to the bootstrap `sign_in` href. + /// Sign in entry: posts the username to the authorize-challenge `sign_in` href. func signInStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest - /// SSPR entry, posted to the bootstrap `reset_password` href. + /// SSPR entry, posted to the authorize-challenge `reset_password` href. func resetPasswordStart(username: String, continuationToken: String, href: String, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index f9b9ba1bac..6c82b700e8 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -75,7 +75,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_happyPath_returnsCodeRequired() async { requestProviderMock.mockRequest() validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-bootstrap", links: ["reset_password": "https://contoso.com/reset"]) + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") ] validatorMock.interactionResponses = [ .challengeRequired(continuationToken: "ct-2", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com"), @@ -95,7 +95,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { XCTAssertTrue(requestProviderMock.challengeCalled) } - func test_resetPassword_whenBootstrapFails_returnsError() async { + func test_resetPassword_whenAuthorizationChallengeFails_returnsError() async { requestProviderMock.mockRequest() validatorMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(kind: .generalError))] @@ -110,7 +110,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_whenUserNotFound_returnsError() async { requestProviderMock.mockRequest() validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-bootstrap", links: ["reset_password": "https://contoso.com/reset"]) + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") ] validatorMock.interactionResponses = [ .error(MSALNativeAuthFlowError(kind: .userNotFound)) @@ -252,7 +252,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { func test_signUp_happyPath_returnsCodeRequired() async { requestProviderMock.mockRequest() validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-bootstrap", links: ["sign_up": "https://contoso.com/signup"]) + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup") ] validatorMock.interactionResponses = [ .codeRequired(continuationToken: "ct-2", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) @@ -271,7 +271,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { let passwordMethod = MSALNativeAuthHALResponse.EmbeddedMethod( id: "1", type: "password", hint: nil, links: ["challenge": "https://contoso.com/pw/challenge"]) validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-bootstrap", links: ["sign_in": "https://contoso.com/signin"]), + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signin"), .authorizationCode(code: "auth-code") ] validatorMock.interactionResponses = [ @@ -298,7 +298,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { let emailMethod = MSALNativeAuthHALResponse.EmbeddedMethod( id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/email/challenge"]) validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-bootstrap", links: ["sign_in": "https://contoso.com/signin"]) + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signin") ] validatorMock.interactionResponses = [ .signInMethods(continuationToken: "ct-2", methods: [emailMethod]), diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift index 6afa72abb6..196d22fd23 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift @@ -35,7 +35,10 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating private(set) var validateInteractionCallCount = 0 private(set) var validateTokenCallCount = 0 - func validateAuthorizeChallenge(_ result: Result) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + func validateAuthorizeChallenge( + _ result: Result, + flowType: MSALNativeAuthV2FlowType + ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { defer { validateAuthorizeChallengeCallCount += 1 } if validateAuthorizeChallengeCallCount < authorizeChallengeResponses.count { return authorizeChallengeResponses[validateAuthorizeChallengeCallCount] diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index ffdc81f999..21582ffb35 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -75,25 +75,34 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateAuthorizeChallenge_withContinuationToken() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(.success(response)) - XCTAssertEqual(result, .continuationToken(continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"])) + let result = sut.validateAuthorizeChallenge(.success(response), flowType: .resetPassword) + XCTAssertEqual(result, .continuationToken(continuationToken: "ct", href: "https://contoso.com/reset")) + } + + func test_validateAuthorizeChallenge_missingFlowLink_returnsError() { + let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) + let result = sut.validateAuthorizeChallenge(.success(response), flowType: .signUp) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Invalid authorize-challenge response: missing 'sign_up' link" + ))) } func test_validateAuthorizeChallenge_withAuthorizationCode() { let response = makeResponse(code: "auth-code") - let result = sut.validateAuthorizeChallenge(.success(response)) + let result = sut.validateAuthorizeChallenge(.success(response), flowType: .signIn) XCTAssertEqual(result, .authorizationCode(code: "auth-code")) } func test_validateAuthorizeChallenge_withServerError_returnsError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateAuthorizeChallenge(.success(response)) + let result = sut.validateAuthorizeChallenge(.success(response), flowType: .signIn) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) } func test_validateAuthorizeChallenge_withTransportFailure_returnsError() { - let result = sut.validateAuthorizeChallenge(.failure(ErrorMock.error)) + let result = sut.validateAuthorizeChallenge(.failure(ErrorMock.error), flowType: .signIn) guard case .error = result else { return XCTFail("Expected error") } From 4d55fc37ab66ae361e9d108feafd6f2ee1fdf4db Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 11:40:15 +0100 Subject: [PATCH 18/85] Added ApiId for each endpoint that is the same --- .../v2/MSALNativeAuthV2FlowController.swift | 30 +++++++++---------- .../MSALNativeAuthTelemetryApiId.swift | 17 ++++++++++- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 8abda841ad..6cc384454b 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -69,7 +69,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { let flowType: MSALNativeAuthV2FlowType = .signUp let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUp, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpStart, context: context) let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + sign_up link). @@ -96,8 +96,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let flowType: MSALNativeAuthV2FlowType = .signIn let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil - ? .telemetryApiIdSignInWithPasswordStart - : .telemetryApiIdSignInWithCodeStart + ? .telemetryApiIdV2SignInWithPasswordStart + : .telemetryApiIdV2SignInWithCodeStart let event = makeAndStartTelemetryEvent(id: apiId, context: context) let scopes = joinScopes(parameters.scopes) @@ -198,7 +198,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { let flowType: MSALNativeAuthV2FlowType = .resetPassword let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordStart, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordStart, context: context) let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + reset_password link). @@ -244,7 +244,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let continuation = state.continuation guard let verifyHref = continuation.link("verify")?.absoluteString else { - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) return failure( .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), event: event, @@ -256,8 +256,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch continuation.flowType { case .signIn, .signUp: let apiId: MSALNativeAuthTelemetryApiId = continuation.flowType == .signUp - ? .telemetryApiIdSignUpSubmitCode - : .telemetryApiIdSignInSubmitCode + ? .telemetryApiIdV2SignUpSubmitCode + : .telemetryApiIdV2SignInSubmitCode let event = makeAndStartTelemetryEvent(id: apiId, context: context) let result = await performInteraction(context: context) { try self.requestProvider.submitCode(href: verifyHref, code: code, continuationToken: continuation.continuationToken, context: context) @@ -272,7 +272,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa recoverableState: state ) case .resetPassword: - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmitCode, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) let result = await performInteraction(context: context) { try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) } @@ -298,7 +298,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignInSubmitPassword, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignInSubmitPassword, context: context) let continuation = state.continuation guard let verifyHref = continuation.link("verify")?.absoluteString else { @@ -331,7 +331,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // swiftlint:disable:next function_body_length func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordSubmit, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmit, context: context) let continuation = state.continuation guard let updateHref = continuation.link("update")?.absoluteString else { @@ -409,7 +409,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdSignUpSubmitAttributes, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpSubmitAttributes, context: context) let continuation = state.continuation guard let submitHref = continuation.link("submitAttributes")?.absoluteString else { @@ -451,7 +451,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // JIT (strong-auth registration) carries an `enroll` link; MFA carries a `challenge` link. if continuation.link("enroll") != nil, let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link("enroll"))?.absoluteString { - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdJITChallenge, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) let result = await performInteraction(context: context) { try self.requestProvider.registerMethod( href: enrollHref, @@ -470,7 +470,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdMFAGetAuthMethods, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link("challenge"))?.absoluteString else { return failure( .error( @@ -519,7 +519,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdMFASubmitChallenge, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFASubmitChallenge, context: context) let continuation = state.continuation // JIT activation uses the `activate` link; MFA uses the `verify` link. @@ -552,7 +552,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdResetPasswordResendCode, context: context) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordResendCode, context: context) let continuation = state.continuation guard let resendHref = continuation.link("resend")?.absoluteString else { diff --git a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift index 2f39a25749..31d70005d0 100644 --- a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift +++ b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift @@ -61,11 +61,26 @@ enum MSALNativeAuthTelemetryApiId: Int { case telemetryApiIdJITChallenge = 75030 case telemetryApiIdJITContinue = 75031 case telemetryApiISignInAfterJIT = 75032 - // Native Auth V2 (server-driven HAL) flows. + // Native Auth V2 (server-driven HAL) network requests. case telemetryApiIdV2AuthorizeChallenge = 76001 case telemetryApiIdV2Token = 76002 case telemetryApiIdV2SignIn = 76003 case telemetryApiIdV2SignUp = 76004 case telemetryApiIdV2ResetPassword = 76005 case telemetryApiIdV2Hal = 76006 + // Native Auth V2 (server-driven HAL) controller operations. + case telemetryApiIdV2SignUpStart = 76007 + case telemetryApiIdV2SignInWithPasswordStart = 76008 + case telemetryApiIdV2SignInWithCodeStart = 76009 + case telemetryApiIdV2ResetPasswordStart = 76010 + case telemetryApiIdV2SignUpSubmitCode = 76011 + case telemetryApiIdV2SignInSubmitCode = 76012 + case telemetryApiIdV2ResetPasswordSubmitCode = 76013 + case telemetryApiIdV2SignInSubmitPassword = 76014 + case telemetryApiIdV2ResetPasswordSubmit = 76015 + case telemetryApiIdV2SignUpSubmitAttributes = 76016 + case telemetryApiIdV2JITChallenge = 76017 + case telemetryApiIdV2MFAGetAuthMethods = 76018 + case telemetryApiIdV2MFASubmitChallenge = 76019 + case telemetryApiIdV2ResetPasswordResendCode = 76020 } From 75d5f2375cf8690ddde324636426dc7eb99bcb9d Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 12:13:28 +0100 Subject: [PATCH 19/85] Refine V2 flow controller: naming, hint fallback, and per-link error clarity - Rename ambiguous token2 to challengeContinuationToken in the sign-in and reset-password start paths. - Thread the authorization-challenge hint through sign-in so codeRequired and activationRequired fall back to the masked destination when the challenge response omits sentTo, matching the reset-password behavior. - Split selectAuthMethod into explicit JIT (enroll) vs MFA (challenge) branches, each owning its own guard, error message, and telemetry id so a missing link is reported against the correct flow. - Split submitChallenge into JIT (activate) vs MFA (verify) so a missing link reports exactly which link was absent instead of "verify/activate". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v2/MSALNativeAuthV2FlowController.swift | 181 ++++++++++-------- 1 file changed, 101 insertions(+), 80 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 6cc384454b..9c39f5285e 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -78,7 +78,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure(authorizationChallenge, event: event, context: context) } - // Sign-up start (auto-triggers the email challenge). let startResult = await performInteraction(context: context) { try self.requestProvider.signUpStart( username: parameters.username, @@ -107,7 +106,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure(authorizationChallenge, event: event, context: context) } - // Sign-in (method discovery). let startResult = await performInteraction(context: context) { try self.requestProvider.signInStart( username: parameters.username, @@ -117,12 +115,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Resolve the token and the method challenge href. - // The server may either return `.signInMethods` (action == nil, methods embedded) so the - // client picks a method, or collapse discovery and return `.challengeRequired` (action == - // "challenge") directly with the chosen method's challenge href already resolved. - let token2: String + let challengeContinuationToken: String let challengeHref: String + let challengeHint: String? switch startResult { case .signInMethods(let token, let methods): @@ -148,11 +143,13 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa context: context ) } - token2 = token + challengeContinuationToken = token challengeHref = href - case .challengeRequired(let token, let href, _): - token2 = token + challengeHint = method.hint + case .challengeRequired(let token, let href, let hint): + challengeContinuationToken = token challengeHref = href + challengeHint = hint default: return await mapInteraction( startResult, @@ -164,13 +161,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Challenge the chosen method. let challengeResult = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) + try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, context: context) } - // When a password was supplied and the password factor is required, submit it now - // so password sign-in completes in one call. if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult { let verifyResult = await performInteraction(context: context) { try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) @@ -191,7 +185,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa username: parameters.username, scopes: scopes, event: event, - context: context + context: context, + fallbackHint: challengeHint ) } @@ -207,7 +202,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure(authorizationChallenge, event: event, context: context) } - // Reset-password start. let startResult = await performInteraction(context: context) { try self.requestProvider.resetPasswordStart( username: parameters.username, @@ -217,13 +211,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - guard case .challengeRequired(let token2, let challengeHref, let hint) = startResult else { + guard case .challengeRequired(let challengeContinuationToken, let challengeHref, let hint) = startResult else { return interactionFailure(startResult, event: event, context: context, newState: nil) } - // Auto-trigger the challenge (send EOTP). let challengeResult = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: token2, context: context) + try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, context: context) } return handleCodeRequired( @@ -252,7 +245,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Sign in / sign up use `code`; reset password uses `otp`. switch continuation.flowType { case .signIn, .signUp: let apiId: MSALNativeAuthTelemetryApiId = continuation.flowType == .signUp @@ -342,7 +334,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - // Update password. let updateResult = await performInteraction(context: context) { try self.requestProvider.updatePassword( href: updateHref, @@ -356,7 +347,6 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return interactionFailure(updateResult, event: event, context: context, newState: nil) } - // Poll until the operation completes. var completionToken: String? for attempt in 0.. 0 { @@ -449,8 +439,20 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let continuation = state.continuation // JIT (strong-auth registration) carries an `enroll` link; MFA carries a `challenge` link. - if continuation.link("enroll") != nil, - let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link("enroll"))?.absoluteString { + if continuation.link("enroll") != nil { + guard let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link("enroll"))?.absoluteString else { + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) + return failure( + .error( + MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Missing enroll link for selected method" + ) + ), + event: event, + context: context + ) + } let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) let result = await performInteraction(context: context) { try self.requestProvider.registerMethod( @@ -468,52 +470,51 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa event: event, context: context ) - } - - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) - guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link("challenge"))?.absoluteString else { - return failure( - .error( - MSALNativeAuthFlowError( - kind: .generalError, - errorDescription: "Missing challenge link for selected method" - ) - ), - event: event, - context: context - ) - } - - let result = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: continuation.continuationToken, context: context) - } + } else { + guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link("challenge"))?.absoluteString else { + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) + return failure( + .error( + MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "Missing challenge link for selected method" + ) + ), + event: event, + context: context + ) + } + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) + let result = await performInteraction(context: context) { + try self.requestProvider.challenge(href: challengeHref, continuationToken: continuation.continuationToken, context: context) + } - // An MFA method challenge surfaces as a verification-required action. - switch result { - case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): - let newState = makeState( - continuation.flowType, - continuationToken: token, - links: ["verify": verifyHref, "resend": resendHref], - username: continuation.username, - sentToHint: sentTo.isEmpty ? continuation.sentToHint : sentTo, - codeLength: codeLength, - scopes: continuation.scopes - ) - stopTelemetryEvent(event, context: context) - return response(.actionRequired( - action: .mfaVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), - newState: newState - ), context: context) - default: - return await mapInteraction( - result, - flowType: continuation.flowType, - username: continuation.username, - scopes: continuation.scopes, - event: event, - context: context - ) + switch result { + case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): + let newState = makeState( + continuation.flowType, + continuationToken: token, + links: ["verify": verifyHref, "resend": resendHref], + username: continuation.username, + sentToHint: sentTo.isEmpty ? continuation.sentToHint : sentTo, + codeLength: codeLength, + scopes: continuation.scopes + ) + stopTelemetryEvent(event, context: context) + return response(.actionRequired( + action: .mfaVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + newState: newState + ), context: context) + default: + return await mapInteraction( + result, + flowType: continuation.flowType, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context + ) + } } } @@ -523,12 +524,25 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let continuation = state.continuation // JIT activation uses the `activate` link; MFA uses the `verify` link. - guard let submitHref = (continuation.link("activate") ?? continuation.link("verify"))?.absoluteString else { - return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify/activate link")), - event: event, - context: context - ) + let submitHref: String + if continuation.link("activate") != nil { + guard let activateHref = continuation.link("activate")?.absoluteString else { + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing activate link")), + event: event, + context: context + ) + } + submitHref = activateHref + } else { + guard let verifyHref = continuation.link("verify")?.absoluteString else { + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + event: event, + context: context + ) + } + submitHref = verifyHref } let result = await performInteraction(context: context) { @@ -669,7 +683,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, - recoverableState: MSALNativeAuthFlowState? = nil + recoverableState: MSALNativeAuthFlowState? = nil, + fallbackHint: String? = nil ) async -> MSALNativeAuthV2FlowControllerResponse { switch result { case .readyToComplete(let token): @@ -687,13 +702,14 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa continuationToken: token, links: ["verify": verifyHref, "resend": resendHref], username: username, - sentToHint: sentTo.isEmpty ? nil : sentTo, + sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, scopes: scopes ) + let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo stopTelemetryEvent(event, context: context) return response(.actionRequired( - action: .codeRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + action: .codeRequired(sentTo: displaySentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), newState: newState ), context: context) case .passwordRequired(let token, let verifyHref): @@ -743,13 +759,18 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa continuationToken: token, links: ["activate": activateHref], username: username, - sentToHint: sentTo.isEmpty ? nil : sentTo, + sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, scopes: scopes ) + let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo stopTelemetryEvent(event, context: context) return response(.actionRequired( - action: .strongAuthVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), + action: .strongAuthVerificationRequired( + sentTo: displaySentTo, + channel: MSALNativeAuthChannelType(value: "email"), + codeLength: codeLength + ), newState: newState ), context: context) case .error(let error): From 09562142dcdb3b5805f61140d4b7bed21c8a1138 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 12:44:15 +0100 Subject: [PATCH 20/85] Extract shared token-response cache persistence into MSALNativeAuthTokenCacher Both native-auth token controllers now delegate token-response caching to a single MSALNativeAuthTokenCacher helper (clear existing account, save tokens, validate account). Account validation is injected as a closure so each controller keeps using its own validator, restoring the post-save account validation that was missing on the server-driven flow path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MSAL/MSAL.xcodeproj/project.pbxproj | 18 ++- .../MSALNativeAuthTokenCacher.swift | 104 ++++++++++++++++++ .../MSALNativeAuthTokenController.swift | 74 ++----------- .../v2/MSALNativeAuthV2FlowController.swift | 43 ++++---- 4 files changed, 148 insertions(+), 91 deletions(-) create mode 100644 MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 45991eafe6..56b898f1d8 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -92,7 +92,6 @@ 12E2160B2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 12E2160C2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; - D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; @@ -402,6 +401,7 @@ 38880DF523280C5A00688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; + 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; 583BFD0F24DC8E670035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; 583BFD1024DC8EE80035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; 583BFD1624DDF9B10035B901 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */; }; @@ -418,7 +418,6 @@ 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; - FADE0000000000000000AA03 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -447,7 +446,6 @@ 7248CF9E2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */; }; 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; - D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; 886F515829CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -997,8 +995,9 @@ BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; - FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; + D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; + D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; D61A64941E5AA7D60086D120 /* MSALTestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64801E5AA7C60086D120 /* MSALTestAppDelegate.m */; }; D61A64951E5AA7D60086D120 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64811E5AA7C60086D120 /* main.m */; }; D61A64A91E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A649D1E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m */; }; @@ -1714,7 +1713,10 @@ E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; + FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; + FADE0000000000000000AA03 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2320,7 +2322,6 @@ 963377BE211E14C600943EE0 /* MSALWebviewType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWebviewType.m; sourceTree = ""; }; 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALResponse.swift; sourceTree = ""; }; - FADE0000000000000000AA01 /* HALResource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HALResource.swift; sourceTree = ""; }; 9648AF54225D826500F66801 /* MSALTelemetryConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTelemetryConfig+Internal.h"; sourceTree = ""; }; 9648AF5B225DD6A900F66801 /* MSALGlobalConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALGlobalConfig+Internal.h"; sourceTree = ""; }; 9682A62A218290F700E37E63 /* MSALDefinitions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDefinitions.h; sourceTree = ""; }; @@ -2380,7 +2381,7 @@ A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAction.swift; sourceTree = ""; }; B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProvider.swift; sourceTree = ""; }; - D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestConfigurator.swift; sourceTree = ""; }; + B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenCacher.swift; sourceTree = ""; }; B203459221AF77FB00B221AA /* MSALRedirectUri.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALRedirectUri.h; sourceTree = ""; }; B203459321AF77FB00B221AA /* MSALRedirectUri.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALRedirectUri.m; sourceTree = ""; }; B203459C21AFA1FB00B221AA /* MSALRedirectUri+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALRedirectUri+Internal.h"; sourceTree = ""; }; @@ -2541,6 +2542,7 @@ C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowController.swift; sourceTree = ""; }; C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorTests.swift; sourceTree = ""; }; + D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestConfigurator.swift; sourceTree = ""; }; D61A63F11E5979200086D120 /* MSALResult+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALResult+Internal.h"; sourceTree = ""; }; D61A64331E5A29580086D120 /* MSAL Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MSAL Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D61A64661E5AA6B40086D120 /* msal__test_app__ios.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = msal__test_app__ios.xcconfig; sourceTree = ""; }; @@ -2924,6 +2926,7 @@ E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUnknownCaseProtocol.swift; sourceTree = ""; }; E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUnknownCaseProtocolTests.swift; sourceTree = ""; }; F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolver.swift; sourceTree = ""; }; + FADE0000000000000000AA01 /* HALResource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HALResource.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3162,6 +3165,7 @@ DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */, E2EFAD152A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift */, 1718011BA0E3C2154663D13F /* v2 */, + B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */, ); path = controllers; sourceTree = ""; @@ -7336,6 +7340,7 @@ 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, + 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7641,6 +7646,7 @@ 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, + F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift new file mode 100644 index 0000000000..0cf9811f83 --- /dev/null +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift @@ -0,0 +1,104 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +@_implementationOnly import MSAL_Private + +import Foundation + +/// Token-response cache persistence. +final class MSALNativeAuthTokenCacher { + + private let cacheAccessor: MSALNativeAuthCacheInterface + + init(cacheAccessor: MSALNativeAuthCacheInterface) { + self.cacheAccessor = cacheAccessor + } + + func cache( + _ tokenResponse: MSIDTokenResponse, + context: MSIDRequestContext, + msidConfiguration: MSIDConfiguration, + validateAccount: (_ tokenResult: MSIDTokenResult, _ accountIdentifier: MSIDAccountIdentifier) throws -> Bool + ) throws -> MSIDTokenResult { + let displayableId = tokenResponse.idTokenObj?.username() + let homeAccountId = tokenResponse.idTokenObj?.userId + + guard let accountIdentifier = MSIDAccountIdentifier(displayableId: displayableId, homeAccountId: homeAccountId) else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error creating account identifier") + throw MSALNativeAuthInternalError.invalidResponse + } + + // Remove any existing account for this configuration before saving the new tokens. + clearExistingAccount(msidConfiguration: msidConfiguration, context: context) + + let savedResult: MSIDTokenResult? + do { + savedResult = try cacheAccessor.validateAndSaveTokensAndAccount( + tokenResponse: tokenResponse, + configuration: msidConfiguration, + context: context + ) + } catch { + MSALNativeAuthLogger.logPII( + level: .warning, + context: context, + format: "Error caching response: \(MSALLogMask.maskEUII(error)) (ignoring)") + savedResult = nil + } + + guard let result = savedResult else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error caching token response") + throw MSALNativeAuthInternalError.invalidResponse + } + + guard try validateAccount(result, accountIdentifier) else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error validating account") + throw MSALNativeAuthInternalError.invalidResponse + } + + return result + } + + private func clearExistingAccount(msidConfiguration: MSIDConfiguration, context: MSIDRequestContext) { + do { + let accounts = try cacheAccessor.getAllAccounts(configuration: msidConfiguration) + if let account = accounts.first { + if let identifier = MSIDAccountIdentifier(displayableId: account.username, homeAccountId: account.identifier) { + try cacheAccessor.clearCache( + accountIdentifier: identifier, + authority: msidConfiguration.authority, + clientId: msidConfiguration.clientId, + context: context) + } + } else { + MSALNativeAuthLogger.log( + level: .warning, + context: context, + format: "Error creating MSIDAccountIdentifier out of MSALAccount (ignoring)") + } + } catch { + MSALNativeAuthLogger.log(level: .warning, context: context, format: "Error clearing previous account (ignoring)") + } + } +} diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift index 4c9d4f13ef..6117602d77 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift @@ -34,6 +34,7 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController { private let requestProvider: MSALNativeAuthTokenRequestProviding private let responseValidator: MSALNativeAuthTokenResponseValidating private let cacheAccessor: MSALNativeAuthCacheInterface + private let tokenCacher: MSALNativeAuthTokenCacher init( clientId: String, @@ -46,6 +47,7 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController { self.factory = factory self.responseValidator = responseValidator self.cacheAccessor = cacheAccessor + self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) super.init( clientId: clientId ) @@ -132,29 +134,13 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController { context: MSIDRequestContext, msidConfiguration: MSIDConfiguration ) throws -> MSIDTokenResult { - let displayableId = tokenResponse.idTokenObj?.username() - let homeAccountId = tokenResponse.idTokenObj?.userId - - guard let accountIdentifier = MSIDAccountIdentifier(displayableId: displayableId, homeAccountId: homeAccountId) else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error creating account identifier") - throw MSALNativeAuthInternalError.invalidResponse - } - - guard let result = cacheTokenResponseRetrieveTokenResult(tokenResponse, - context: context, - msidConfiguration: msidConfiguration) else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error caching token response") - throw MSALNativeAuthInternalError.invalidResponse - } - - guard try responseValidator.validateAccount(with: result, - context: context, - accountIdentifier: accountIdentifier) else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error validating account") - throw MSALNativeAuthInternalError.invalidResponse + return try tokenCacher.cache( + tokenResponse, + context: context, + msidConfiguration: msidConfiguration + ) { [responseValidator] tokenResult, accountIdentifier in + try responseValidator.validateAccount(with: tokenResult, context: context, accountIdentifier: accountIdentifier) } - - return result } } @@ -162,50 +148,6 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController { // name similarity with another Objective C function when building for Release extension MSALNativeAuthTokenController { - private func cacheTokenResponseRetrieveTokenResult( - _ tokenResponse: MSIDTokenResponse, - context: MSIDRequestContext, - msidConfiguration: MSIDConfiguration - ) -> MSIDTokenResult? { - do { - // If there is an account existing already in the cache, we remove it - try clearAccount(msidConfiguration: msidConfiguration, context: context) - } catch { - MSALNativeAuthLogger.logPII(level: .warning, context: context, format: "Error clearing account \(MSALLogMask.maskEUII(error)) (ignoring)") - } - do { - let result = try cacheAccessor.validateAndSaveTokensAndAccount(tokenResponse: tokenResponse, - configuration: msidConfiguration, - context: context) - return result - } catch { - MSALNativeAuthLogger.logPII(level: .warning, - context: context, - format: "Error caching response: \(MSALLogMask.maskEUII(error)) (ignoring)") - } - return nil - } - - private func clearAccount(msidConfiguration: MSIDConfiguration, context: MSIDRequestContext) throws { - do { - let accounts = try cacheAccessor.getAllAccounts(configuration: msidConfiguration) - if let account = accounts.first { - if let identifier = MSIDAccountIdentifier(displayableId: account.username, homeAccountId: account.identifier) { - try cacheAccessor.clearCache(accountIdentifier: identifier, - authority: msidConfiguration.authority, - clientId: msidConfiguration.clientId, - context: context) - } - } else { - MSALNativeAuthLogger.log(level: .warning, - context: context, - format: "Error creating MSIDAccountIdentifier out of MSALAccount (ignoring)") - } - } catch { - MSALNativeAuthLogger.log(level: .warning, context: context, format: "Error clearing previous account (ignoring)") - } - } - private func performTokenRequest( _ request: MSIDHttpRequest, context: MSIDRequestContext diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 9c39f5285e..d10f9f7c51 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -35,6 +35,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa private let responseValidator: MSALNativeAuthV2ResponseValidating private let cacheAccessor: MSALNativeAuthCacheInterface private let resultFactory: MSALNativeAuthResultBuildable + private let tokenCacher: MSALNativeAuthTokenCacher private let kNumberOfTimesToRetryPollCompletionCall = 5 private let pollIntervalNanoseconds: UInt64 = 1_500_000_000 // 1.5s @@ -51,6 +52,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa self.responseValidator = responseValidator self.cacheAccessor = cacheAccessor self.resultFactory = resultFactory + self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) super.init(clientId: config.clientId) } @@ -863,33 +865,36 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } - /// Persists the token response (tokens + account) to the shared MSAL cache and returns the - /// resulting `MSIDTokenResult`. private func cacheTokenResponse( _ tokenResponse: MSIDTokenResponse, context: MSALNativeAuthRequestContext, msidConfiguration: MSIDConfiguration ) throws -> MSIDTokenResult { - // Remove any existing account for this configuration before saving the new tokens. - if let accounts = try? cacheAccessor.getAllAccounts(configuration: msidConfiguration), - let account = accounts.first, - let identifier = MSIDAccountIdentifier(displayableId: account.username, homeAccountId: account.identifier) { - try? cacheAccessor.clearCache( - accountIdentifier: identifier, - authority: msidConfiguration.authority, - clientId: msidConfiguration.clientId, - context: context - ) + return try tokenCacher.cache( + tokenResponse, + context: context, + msidConfiguration: msidConfiguration + ) { tokenResult, accountIdentifier in + try self.validateAccount(tokenResult, accountIdentifier: accountIdentifier, context: context) } + } - guard let tokenResult = try cacheAccessor.validateAndSaveTokensAndAccount( - tokenResponse: tokenResponse, - configuration: msidConfiguration, - context: context - ) else { - throw MSALNativeAuthInternalError.invalidResponse + private func validateAccount( + _ tokenResult: MSIDTokenResult, + accountIdentifier: MSIDAccountIdentifier, + context: MSALNativeAuthRequestContext + ) throws -> Bool { + var error: NSError? + let validAccount = MSIDTokenResponseValidator().validateAccount( + accountIdentifier, + tokenResult: tokenResult, + correlationID: context.correlationId(), + error: &error + ) + if let error = error { + throw error } - return tokenResult + return validAccount } /// Extracts the granted scopes from a token response so the cache target matches what was issued. From 15ec8ddd8bfc8f011e47a8f82b1efaee5b9157ed Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 15:35:28 +0100 Subject: [PATCH 21/85] Deduplicate native-auth controllers and wire HAL isError propagation Share token-request handling between the token controller and the V2 flow controller via a MSALNativeAuthTokenRequestHandling protocol with a default implementation, keeping performTokenRequest off the base controller. Extract the request interceptor bridge into a single shared type used by both the V1 and V2 request configurators. Route V2 response errors through isError so the server error is surfaced up the chain in all validator paths, and drop unused HALResource link accessors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- MSAL/MSAL.xcodeproj/project.pbxproj | 12 ++++ .../MSALNativeAuthBaseController.swift | 12 ++++ .../MSALNativeAuthTokenController.swift | 44 +------------ .../MSALNativeAuthTokenRequestHandling.swift | 65 +++++++++++++++++++ .../v2/MSALNativeAuthV2FlowController.swift | 57 ++-------------- .../MSALNativeAuthRequestConfigurator.swift | 18 +---- ...ALNativeAuthRequestInterceptorBridge.swift | 43 ++++++++++++ .../network/responses/v2/HALResource.swift | 10 --- .../MSALNativeAuthV2ResponseValidator.swift | 19 ++++-- .../MSALNativeAuthV2RequestConfigurator.swift | 21 +----- 10 files changed, 155 insertions(+), 146 deletions(-) create mode 100644 MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift create mode 100644 MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 56b898f1d8..5e1e49d1af 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -397,9 +397,11 @@ 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */; }; 33A0542A5B652892314FD6C8 /* MSALNativeAuthV2FlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */; }; + 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; 38880DF423280C5900688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 38880DF523280C5A00688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; 583BFD0F24DC8E670035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; @@ -1029,6 +1031,7 @@ D65A6FAC1E3FF3D900C69FBA /* MSALResult.h in Headers */ = {isa = PBXBuildFile; fileRef = D65A6F851E3FF3D900C69FBA /* MSALResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; D65A6FAD1E3FF3D900C69FBA /* MSALAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = D65A6F861E3FF3D900C69FBA /* MSALAccount.h */; settings = {ATTRIBUTES = (Public, ); }; }; D65A6FD51E3FF49C00C69FBA /* MSAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F501E3FD32D00C69FBA /* MSAL.framework */; }; + D661864E47CDC268A2FA5EEF /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */; }; D67227961EBD10B500F3422A /* MSAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; }; D67227971EBD10B500F3422A /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D67227A31EBD111900F3422A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D67227A21EBD111900F3422A /* main.m */; }; @@ -1582,6 +1585,7 @@ DEFE87732CA6BC91009D11DC /* CredentialsDelegateSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876E2CA6BC91009D11DC /* CredentialsDelegateSpies.swift */; }; DEFE87742CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; E04298BA8ED8FBE431F561A2 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */; }; + E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */; }; E2025CC92B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025CC82B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift */; }; E2025D202B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025D1F2B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift */; }; E205D62E29B783FF003887BC /* MSALNativeAuthInternalConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E205D62D29B783FF003887BC /* MSALNativeAuthInternalConfiguration.swift */; }; @@ -2256,6 +2260,7 @@ 28FDC4A82A38C0D000E38BE1 /* SignInAfterSignUpError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInAfterSignUpError.swift; sourceTree = ""; }; 28FDC4AB2A38D7D200E38BE1 /* MSALNativeAuthSignInControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInControllerMock.swift; sourceTree = ""; }; 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseErrorHandler.swift; sourceTree = ""; }; + 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenRequestHandling.swift; sourceTree = ""; }; 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; @@ -2539,6 +2544,7 @@ B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALWPJMetaData+Internal.h"; sourceTree = ""; }; B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControlling.swift; sourceTree = ""; }; B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowDelegate.swift; sourceTree = ""; }; + B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequestInterceptorBridge.swift; sourceTree = ""; }; C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowController.swift; sourceTree = ""; }; C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorTests.swift; sourceTree = ""; }; @@ -3166,6 +3172,7 @@ E2EFAD152A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift */, 1718011BA0E3C2154663D13F /* v2 */, B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */, + 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */, ); path = controllers; sourceTree = ""; @@ -5322,6 +5329,7 @@ E235613329C9D528000E01CA /* MSALNativeAuthInternalChallengeType.swift */, 289C1D8B2DE899B7009EEBEA /* MSALNativeAuthInternalCapability.swift */, 03D8CB5A8588FA78B92F2A2D /* v2 */, + B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */, ); path = network; sourceTree = ""; @@ -7341,6 +7349,8 @@ 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */, + E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, + 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7647,6 +7657,8 @@ 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */, + D661864E47CDC268A2FA5EEF /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, + 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift index a8b3ffb276..e0a43571b2 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift @@ -24,6 +24,8 @@ @_implementationOnly import MSAL_Private +import Foundation + class MSALNativeAuthBaseController { typealias TelemetryInfo = (event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext) @@ -35,6 +37,16 @@ class MSALNativeAuthBaseController { self.clientId = clientId } + func joinScopes(_ scopes: [String]?) -> [String] { + let defaultOIDCScopes = MSALPublicClientApplication.defaultOIDCScopes().array + guard let scopes = scopes else { + return defaultOIDCScopes as? [String] ?? [] + } + let joinedScopes = NSMutableOrderedSet(array: scopes) + joinedScopes.addObjects(from: defaultOIDCScopes) + return joinedScopes.array as? [String] ?? [] + } + func makeAndStartTelemetryEvent( id: MSALNativeAuthTelemetryApiId, context: MSIDRequestContext diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift index 6117602d77..cda9aabdfa 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift @@ -26,7 +26,7 @@ import Foundation -class MSALNativeAuthTokenController: MSALNativeAuthBaseController { +class MSALNativeAuthTokenController: MSALNativeAuthBaseController, MSALNativeAuthTokenRequestHandling { // MARK: - Variables @@ -63,16 +63,6 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController { ) } - func joinScopes(_ scopes: [String]?) -> [String] { - let defaultOIDCScopes = MSALPublicClientApplication.defaultOIDCScopes().array - guard let scopes = scopes else { - return defaultOIDCScopes as? [String] ?? [] - } - let joinedScopes = NSMutableOrderedSet(array: scopes) - joinedScopes.addObjects(from: defaultOIDCScopes) - return joinedScopes.array as? [String] ?? [] - } - func createTokenRequest( username: String? = nil, password: String? = nil, @@ -143,35 +133,3 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController { } } } - -// Extension is required because Swift compiler throws an error due to -// name similarity with another Objective C function when building for Release -extension MSALNativeAuthTokenController { - - private func performTokenRequest( - _ request: MSIDHttpRequest, - context: MSIDRequestContext - ) async -> Result { - return await withCheckedContinuation { continuation in - request.send { response, error in - if let error = error { - continuation.resume(returning: .failure(error)) - return - } - guard let responseDict = response as? [AnyHashable: Any] else { - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) - return - } - do { - let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) - // use request correlation id if server doesn't return one - tokenResponse.correlationId = tokenResponse.correlationId ?? request.context?.correlationId().uuidString - continuation.resume(returning: .success(tokenResponse)) - } catch { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error token request - Both result and error are nil") - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) - } - } - } - } -} diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift new file mode 100644 index 0000000000..879cd2e62f --- /dev/null +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +@_implementationOnly import MSAL_Private + +import Foundation + +protocol MSALNativeAuthTokenRequestHandling { + + func performTokenRequest( + _ request: MSIDHttpRequest, + context: MSIDRequestContext + ) async -> Result +} + +extension MSALNativeAuthTokenRequestHandling { + + func performTokenRequest( + _ request: MSIDHttpRequest, + context: MSIDRequestContext + ) async -> Result { + return await withCheckedContinuation { continuation in + request.send { response, error in + if let error = error { + continuation.resume(returning: .failure(error)) + return + } + guard let responseDict = response as? [AnyHashable: Any] else { + continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) + return + } + do { + let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) + // use request correlation id if server doesn't return one + tokenResponse.correlationId = tokenResponse.correlationId ?? request.context?.correlationId().uuidString + continuation.resume(returning: .success(tokenResponse)) + } catch { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error token request - Both result and error are nil") + continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) + } + } + } + } +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index d10f9f7c51..ba89a6edea 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -28,7 +28,7 @@ import Foundation // swiftlint:disable file_length // swiftlint:disable:next type_body_length -final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNativeAuthV2FlowControlling { +final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNativeAuthV2FlowControlling, MSALNativeAuthTokenRequestHandling { private let config: MSALNativeAuthInternalConfiguration private let requestProvider: MSALNativeAuthV2RequestProviding @@ -829,7 +829,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } - /// Sends the `/token` request and parses the raw OAuth JSON into an `MSIDTokenResponse`. + /// Builds the `/token` request and delegates the send/parse to the shared token-request handler. private func performTokenExchange( code: String, scopes: [String], @@ -842,27 +842,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return .failure(error) } - let fallbackCorrelationId = request.context?.correlationId().uuidString - - return await withCheckedContinuation { continuation in - request.send { response, error in - if let error = error { - continuation.resume(returning: .failure(error)) - return - } - guard let responseDict = response as? [AnyHashable: Any] else { - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) - return - } - do { - let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) - tokenResponse.correlationId = tokenResponse.correlationId ?? fallbackCorrelationId - continuation.resume(returning: .success(tokenResponse)) - } catch { - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) - } - } - } + return await performTokenRequest(request, context: context).map { $0 as MSIDTokenResponse } } private func cacheTokenResponse( @@ -941,20 +921,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return MSALNativeAuthFlowState(continuation: continuation, controller: self) } - /// Merges the caller-requested scopes with the default OIDC scopes (openid, profile, - /// offline_access), de-duplicating while preserving order. - private func joinScopes(_ scopes: [String]?) -> [String] { - let defaultOIDCScopes = MSALPublicClientApplication.defaultOIDCScopes().array - guard let scopes = scopes else { - return defaultOIDCScopes as? [String] ?? [] - } - let joinedScopes = NSMutableOrderedSet(array: scopes) - joinedScopes.addObjects(from: defaultOIDCScopes) - return joinedScopes.array as? [String] ?? [] - } - /// Converts embedded HAL methods into public ``MSALAuthMethod`` objects plus a map of each - /// method's `challenge` href (keyed by method id) for later selection. + /// method's action href (keyed by method id) for later selection. MFA methods carry a + /// `challenge` link; JIT registration methods carry an `enroll`/`register` link. private func authMethods( from methods: [MSALNativeAuthHALResponse.EmbeddedMethod] ) -> (methods: [MSALAuthMethod], methodLinks: [String: String]) { @@ -969,8 +938,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa channelTargetType: MSALNativeAuthChannelType(value: type), loginHint: method.hint )) - if let challenge = method.links["challenge"] { - methodLinks[id] = challenge + if let link = method.links["challenge"] ?? method.links["enroll"] ?? method.links["register"] { + methodLinks[id] = link } } return (out, methodLinks) @@ -1028,16 +997,4 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa stopTelemetryEvent(event, context: context, error: error) return response(.error(error: error, newState: newState), context: context) } - - private func notImplemented( - apiId: MSALNativeAuthTelemetryApiId, - correlationId: UUID?, - flow: String - ) -> MSALNativeAuthV2FlowControllerResponse { - let context = MSALNativeAuthRequestContext(correlationId: correlationId) - let event = makeAndStartTelemetryEvent(id: apiId, context: context) - let error = MSALNativeAuthFlowError(kind: .notImplemented, errorDescription: "\(flow) is not implemented yet.") - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) - } } diff --git a/MSAL/src/native_auth/network/MSALNativeAuthRequestConfigurator.swift b/MSAL/src/native_auth/network/MSALNativeAuthRequestConfigurator.swift index 5ec9489dfc..869b440b7e 100644 --- a/MSAL/src/native_auth/network/MSALNativeAuthRequestConfigurator.swift +++ b/MSAL/src/native_auth/network/MSALNativeAuthRequestConfigurator.swift @@ -320,26 +320,10 @@ class MSALNativeAuthRequestConfigurator: MSIDAADRequestConfigurator { ) throw MSALNativeAuthInternalError.invalidRequest } - + if let interceptor = config.requestInterceptor { request.requestInterceptor = MSALNativeAuthRequestInterceptorBridge(interceptor: interceptor) } configure(request) } } - -/// Bridges MSALNativeAuthRequestInterceptor (Swift public protocol) to MSIDHttpRequestInterceptorProtocol (ObjC). -private final class MSALNativeAuthRequestInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { - - private let interceptor: MSALNativeAuthRequestInterceptor - - init(interceptor: MSALNativeAuthRequestInterceptor) { - self.interceptor = interceptor - } - - func addAdditionalHeaderFields(for requestUrl: URL?, with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock) { - interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in - completionBlock(additionalHeaders) - } - } -} diff --git a/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift b/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift new file mode 100644 index 0000000000..06b84b114b --- /dev/null +++ b/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +@_implementationOnly import MSAL_Private + +/// Bridges `MSALNativeAuthRequestInterceptor` (Swift public protocol) to `MSIDHttpRequestInterceptorProtocol` (ObjC). +final class MSALNativeAuthRequestInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { + + private let interceptor: MSALNativeAuthRequestInterceptor + + init(interceptor: MSALNativeAuthRequestInterceptor) { + self.interceptor = interceptor + } + + func addAdditionalHeaderFields(for requestUrl: URL?, with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock) { + interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in + completionBlock(additionalHeaders) + } + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/HALResource.swift b/MSAL/src/native_auth/network/responses/v2/HALResource.swift index 849666d96a..99ff4dac7e 100644 --- a/MSAL/src/native_auth/network/responses/v2/HALResource.swift +++ b/MSAL/src/native_auth/network/responses/v2/HALResource.swift @@ -105,16 +105,6 @@ internal struct HALResource { // MARK: - Accessors - /// Returns a single link for the given relation, or nil if not present. - func link(rel: String) -> HALLink? { - return links[rel]?.first - } - - /// Returns all links for the given relation. - func allLinks(rel: String) -> [HALLink] { - return links[rel] ?? [] - } - /// Returns embedded resources for the given relation. func embeddedResources(rel: String) -> [[String: Any]] { return embedded[rel] ?? [] diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 062dbaf052..489af51144 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -44,8 +44,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error)) case .success(let response): - if let error = response.error { - return .error(flowError(from: error)) + if response.isError { + return .error(flowError(from: response)) } if let code = response.code { return .authorizationCode(code: code) @@ -75,8 +75,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error)) case .success(let response): - if let error = response.error { - return .error(flowError(from: error)) + if response.isError { + return .error(flowError(from: response)) } if response.state == "continue" { @@ -186,8 +186,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error)) case .success(let response): - if let error = response.error { - return .error(flowError(from: error)) + if response.isError { + return .error(flowError(from: response)) } return .success(accessToken: response.accessToken) } @@ -204,6 +204,13 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin )) } + private func flowError(from response: MSALNativeAuthHALResponse) -> MSALNativeAuthFlowError { + guard let serverError = response.error else { + return MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Server returned an error response") + } + return flowError(from: serverError) + } + private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { let message = serverError.message let errorCodes = estsErrorCodes(from: message) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift index fd12622b54..6ee7c1c1a6 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -234,28 +234,9 @@ final class MSALNativeAuthV2RequestConfigurator: MSIDAADRequestConfigurator { request.errorHandler = MSALNativeAuthV2ResponseErrorHandler() if let interceptor = config.requestInterceptor { - request.requestInterceptor = MSALNativeAuthV2RequestInterceptorBridge(interceptor: interceptor) + request.requestInterceptor = MSALNativeAuthRequestInterceptorBridge(interceptor: interceptor) } return request } } - -/// Bridges `MSALNativeAuthRequestInterceptor` (Swift public protocol) to `MSIDHttpRequestInterceptorProtocol` (ObjC). -private final class MSALNativeAuthV2RequestInterceptorBridge: NSObject, MSIDHttpRequestInterceptorProtocol { - - private let interceptor: MSALNativeAuthRequestInterceptor - - init(interceptor: MSALNativeAuthRequestInterceptor) { - self.interceptor = interceptor - } - - func addAdditionalHeaderFields( - for requestUrl: URL?, - with completionBlock: @escaping MSIDHttpRequestInterceptorAddHeaderCompletionBlock - ) { - interceptor.addAdditionalHeaderFields(requestUrl) { additionalHeaders in - completionBlock(additionalHeaders) - } - } -} From 396d7e78271ffe84b9f9c4d4dfcb8d46d0bd6f6d Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 13 Jul 2026 15:41:03 +0100 Subject: [PATCH 22/85] Drop redundant HAL isError in favor of direct error binding Remove the isError computed property and bind response.error directly in the V2 response validator, dropping the extra flowError overload and its defensive nil branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v2/MSALNativeAuthHALResponse.swift | 5 ----- .../MSALNativeAuthV2ResponseValidator.swift | 19 ++++++------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 1995f371f2..9f6fe635dd 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -88,11 +88,6 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let error: ServerError? - /// Whether this response represents a server error. - var isError: Bool { - return error != nil - } - func href(forRelation relation: String) -> String? { return links[relation] } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 489af51144..062dbaf052 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -44,8 +44,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error)) case .success(let response): - if response.isError { - return .error(flowError(from: response)) + if let error = response.error { + return .error(flowError(from: error)) } if let code = response.code { return .authorizationCode(code: code) @@ -75,8 +75,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error)) case .success(let response): - if response.isError { - return .error(flowError(from: response)) + if let error = response.error { + return .error(flowError(from: error)) } if response.state == "continue" { @@ -186,8 +186,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error)) case .success(let response): - if response.isError { - return .error(flowError(from: response)) + if let error = response.error { + return .error(flowError(from: error)) } return .success(accessToken: response.accessToken) } @@ -204,13 +204,6 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin )) } - private func flowError(from response: MSALNativeAuthHALResponse) -> MSALNativeAuthFlowError { - guard let serverError = response.error else { - return MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Server returned an error response") - } - return flowError(from: serverError) - } - private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { let message = serverError.message let errorCodes = estsErrorCodes(from: message) From 1de98f69ac652abb5a3e3bcc37060a0433cce154 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 15 Jul 2026 14:21:13 +0100 Subject: [PATCH 23/85] MFA --- .../v2/MSALNativeAuthV2FlowController.swift | 51 ++++++++++--------- .../v2/MSALNativeAuthHALResponse.swift | 4 ++ ...SALNativeAuthV2HALResponseSerializer.swift | 2 + .../MSALNativeAuthV2ResponseValidator.swift | 10 ++++ .../MSALNativeAuthV2FlowControllerTests.swift | 20 ++++++++ ...ALNativeAuthV2ResponseValidatorTests.swift | 15 ++++++ 6 files changed, 79 insertions(+), 23 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index ba89a6edea..9a7f64cefa 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -525,35 +525,40 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFASubmitChallenge, context: context) let continuation = state.continuation - // JIT activation uses the `activate` link; MFA uses the `verify` link. + // JIT activation uses the `activate` link and submits the code via the `code` field; + // MFA uses the `verify` link and submits the code via the `otp` field. let submitHref: String - if continuation.link("activate") != nil { - guard let activateHref = continuation.link("activate")?.absoluteString else { - return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing activate link")), - event: event, - context: context - ) - } + let isActivation: Bool + if let activateHref = continuation.link("activate")?.absoluteString { submitHref = activateHref - } else { - guard let verifyHref = continuation.link("verify")?.absoluteString else { - return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), - event: event, - context: context - ) - } + isActivation = true + } else if let verifyHref = continuation.link("verify")?.absoluteString { submitHref = verifyHref + isActivation = false + } else { + return failure( + .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + event: event, + context: context + ) } let result = await performInteraction(context: context) { - try self.requestProvider.submitCode( - href: submitHref, - code: challenge, - continuationToken: continuation.continuationToken, - context: context - ) + if isActivation { + return try self.requestProvider.submitCode( + href: submitHref, + code: challenge, + continuationToken: continuation.continuationToken, + context: context + ) + } else { + return try self.requestProvider.verify( + href: submitHref, + otp: challenge, + continuationToken: continuation.continuationToken, + context: context + ) + } } return await mapInteraction( result, diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 9f6fe635dd..2a26a45e0b 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -69,6 +69,10 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let codeLength: Int? let hint: String? + /// The authentication factor required by a `challenge` action, taken from + /// `challengeContext.authenticationFactor` (e.g. "multiFactor" when MFA is required). + let authenticationFactor: String? + /// Top-level method identifier (`id`) on method-style responses (sign up `start`, JIT `activate`). let methodId: String? /// Top-level method type (`type`, e.g. "email") on method-style responses. diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 8caaca0636..17c4df4444 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -48,6 +48,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali continuationToken: nil, codeLength: nil, hint: nil, + authenticationFactor: nil, methodId: nil, methodType: nil, attributes: [], @@ -74,6 +75,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali continuationToken: resource.string(forKey: "continuationToken") ?? resource.string(forKey: "continuation_token"), codeLength: json["codeLength"] as? Int, hint: resource.string(forKey: "hint"), + authenticationFactor: (json["challengeContext"] as? [String: Any])?["authenticationFactor"] as? String, methodId: resource.string(forKey: "id"), methodType: resource.string(forKey: "type"), attributes: parseAttributes(from: json), diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 062dbaf052..8d1d21a2e6 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -101,6 +101,16 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin guard let challengeHref = method?.links["challenge"] ?? response.href(forRelation: "challenge") else { return missingLink("challenge") } + // MFA required: the server sets challengeContext.authenticationFactor to "multiFactor" + // and embeds the available second-factor methods. Surface them for method selection + // rather than auto-triggering a single challenge. + if response.authenticationFactor == "multiFactor", !response.methods.isEmpty { + return .mfaRequired( + continuationToken: continuationToken, + methods: response.methods, + challengeHref: challengeHref + ) + } return .challengeRequired( continuationToken: continuationToken, challengeHref: challengeHref, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index 6c82b700e8..7dc432011e 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -355,7 +355,27 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { guard case .completed = response.result else { return XCTFail("Expected completed, got \(response.result)") } + // MFA uses the `verify` link and submits the code via the `otp` field. + XCTAssertTrue(requestProviderMock.verifyCalled) + XCTAssertFalse(requestProviderMock.submitCodeCalled) + XCTAssertTrue(requestProviderMock.tokenCalled) + } + + func test_submitChallenge_whenActivateLink_usesSubmitCode() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] + validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] + validatorMock.tokenResponse = .success(accessToken: "access-token") + let state = makeState(flowType: .signIn, links: ["activate": URL(string: "https://contoso.com/jit/activate")!]) + + let response = await sut.submitChallenge("12345678", state: state) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") + } + // JIT activation uses the `activate` link and submits the code via the `code` field. XCTAssertTrue(requestProviderMock.submitCodeCalled) + XCTAssertFalse(requestProviderMock.verifyCalled) XCTAssertTrue(requestProviderMock.tokenCalled) } } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 21582ffb35..094753e29b 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -43,6 +43,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { continuationToken: String? = nil, codeLength: Int? = nil, hint: String? = nil, + authenticationFactor: String? = nil, methodId: String? = nil, methodType: String? = nil, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry] = [], @@ -60,6 +61,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { continuationToken: continuationToken, codeLength: codeLength, hint: hint, + authenticationFactor: authenticationFactor, methodId: methodId, methodType: methodType, attributes: attributes, @@ -117,6 +119,19 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { XCTAssertEqual(result, .challengeRequired(continuationToken: "ct", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com")) } + func test_validateInteraction_challengeAction_multiFactor_returnsMFARequired() { + let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) + let response = makeResponse( + state: "interactionRequired", + action: "challenge", + continuationToken: "ct", + authenticationFactor: "multiFactor", + methods: [method] + ) + let result = sut.validateInteraction(.success(response)) + XCTAssertEqual(result, .mfaRequired(continuationToken: "ct", methods: [method], challengeHref: "https://contoso.com/challenge")) + } + func test_validateInteraction_verifyAction_returnsCodeRequired() { let response = makeResponse( state: "interactionRequired", From 6b4481265ee4824cfb3a360fd6ca7dba751f0930 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 16 Jul 2026 09:49:27 +0100 Subject: [PATCH 24/85] SignUp changes --- .../v2/MSALNativeAuthV2FlowController.swift | 2 +- .../MSALNativeAuthV2FlowControllerTests.swift | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 9a7f64cefa..b266f90511 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -254,7 +254,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa : .telemetryApiIdV2SignInSubmitCode let event = makeAndStartTelemetryEvent(id: apiId, context: context) let result = await performInteraction(context: context) { - try self.requestProvider.submitCode(href: verifyHref, code: code, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) } return await mapInteraction( result, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index 7dc432011e..1517abcaaf 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -173,6 +173,40 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { XCTAssertFalse(requestProviderMock.verifyCalled) } + func test_submitCode_signIn_usesVerifyOtpAndNotSubmitCode() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] + validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] + validatorMock.tokenResponse = .success(accessToken: "access-token") + let state = makeState(flowType: .signIn, links: ["verify": URL(string: "https://contoso.com/verify")!]) + + let response = await sut.submitCode("12345678", state: state) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") + } + // The `/auth/methods/{type}/{id}/verify` endpoint expects the code in the `otp` field. + XCTAssertTrue(requestProviderMock.verifyCalled) + XCTAssertFalse(requestProviderMock.submitCodeCalled) + } + + func test_submitCode_signUp_usesVerifyOtpAndNotSubmitCode() async { + requestProviderMock.mockRequest() + validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] + validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] + validatorMock.tokenResponse = .success(accessToken: "access-token") + let state = makeState(flowType: .signUp, links: ["verify": URL(string: "https://contoso.com/verify")!]) + + let response = await sut.submitCode("12345678", state: state) + + guard case .completed = response.result else { + return XCTFail("Expected completed, got \(response.result)") + } + // The `/auth/methods/{type}/{id}/verify` endpoint expects the code in the `otp` field. + XCTAssertTrue(requestProviderMock.verifyCalled) + XCTAssertFalse(requestProviderMock.submitCodeCalled) + } + // MARK: - submitNewPassword (poll -> token -> completed) func test_submitNewPassword_happyPath_returnsCompleted() async { From 17cc92cc3b6725b7e41bd172e82db5075f59cc34 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 16 Jul 2026 11:28:37 +0100 Subject: [PATCH 25/85] Native Auth V2: sign-up attribute autofill + otp verify fixes Auto-submit the email/password (and any pre-supplied attributes) the app provides at sign-up start when the server issues collectAttributes for them, keeping the values private (internal continuation state, never surfaced to the app, never logged). The full value set is retained for the whole flow; only the attributes the server requests in each step are sent. Track auto-submitted attribute ids and, if the server re-requests an attribute already submitted (e.g. after a validation failure), surface a general error to the app instead of resending in a loop. The error names the attribute id only, never the value. Also fix the /auth/methods/{type}/{id}/verify endpoint to send the code in the `otp` field (not `code`) for sign-in and sign-up, matching the server contract (previously returned AADSTS90023). MFA verify uses `verify`/`otp`; the JIT `activate` link keeps `submitCode`/`code`. Adds regression tests for the autofill happy path, the repeat-attribute error, and the otp verify field for sign-in/sign-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e612cb1a-744b-4c23-8156-b1ac27d3ccad --- .../v2/MSALNativeAuthV2FlowController.swift | 141 ++++++++++++++++-- .../v2/MSALNativeAuthV2FlowResult.swift | 16 +- .../MSALNativeAuthV2FlowControllerTests.swift | 70 +++++++++ .../MSALNativeAuthV2RequestProviderMock.swift | 4 + 4 files changed, 216 insertions(+), 15 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index b266f90511..3650e55845 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -89,7 +89,25 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - return await mapInteraction(startResult, flowType: flowType, username: parameters.username, scopes: scopes, event: event, context: context) + // The APIs request attributes at specific parts of the SingUp process + // so they must be carried privately for the whole flow + var autofillValues: [String: Any] = ["email": parameters.username] + if let attributes = parameters.attributes { + autofillValues.merge(attributes) { _, new in new } + } + if let password = parameters.password { + autofillValues["password"] = password + } + + return await mapInteraction( + startResult, + flowType: flowType, + username: parameters.username, + scopes: scopes, + event: event, + context: context, + signUpAutofillValues: autofillValues + ) } // swiftlint:disable:next function_body_length @@ -234,6 +252,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Continuation + // swiftlint:disable:next function_body_length func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let continuation = state.continuation @@ -263,7 +282,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: continuation.scopes, event: event, context: context, - recoverableState: state + recoverableState: state, + signUpAutofillValues: continuation.signUpAutofillValues, + signUpAutofillSubmittedIds: continuation.signUpAutofillSubmittedIds ) case .resetPassword: let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) @@ -427,7 +448,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: continuation.scopes, event: event, context: context, - recoverableState: state + recoverableState: state, + signUpAutofillValues: continuation.signUpAutofillValues, + signUpAutofillSubmittedIds: continuation.signUpAutofillSubmittedIds ) } @@ -682,7 +705,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // Maps a validated interaction response onto a controller response (the unified, server-driven // branch used by sign in / sign up / MFA / JIT continuation steps). On a terminal `continue` // state it runs the completion (authorize-challenge → token) sequence. - // swiftlint:disable:next function_body_length + // swiftlint:disable:next function_body_length cyclomatic_complexity private func mapInteraction( _ result: MSALNativeAuthV2InteractionValidatedResponse, flowType: MSALNativeAuthV2FlowType, @@ -691,7 +714,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, recoverableState: MSALNativeAuthFlowState? = nil, - fallbackHint: String? = nil + fallbackHint: String? = nil, + signUpAutofillValues: [String: Any]? = nil, + signUpAutofillSubmittedIds: Set = [] ) async -> MSALNativeAuthV2FlowControllerResponse { switch result { case .readyToComplete(let token): @@ -711,7 +736,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, - scopes: scopes + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo stopTelemetryEvent(event, context: context) @@ -720,15 +747,71 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa newState: newState ), context: context) case .passwordRequired(let token, let verifyHref): - let newState = makeState(flowType, continuationToken: token, links: ["verify": verifyHref], username: username, scopes: scopes) + let newState = makeState( + flowType, + continuationToken: token, + links: ["verify": verifyHref], + username: username, + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds + ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .passwordRequired, newState: newState), context: context) case .updateRequired(let token, let updateHref): - let newState = makeState(flowType, continuationToken: token, links: ["update": updateHref], username: username, scopes: scopes) + let newState = makeState( + flowType, + continuationToken: token, + links: ["update": updateHref], + username: username, + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds + ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) case .attributesRequired(let token, let attributes, let submitHref): - let newState = makeState(flowType, continuationToken: token, links: ["submitAttributes": submitHref], username: username, scopes: scopes) + // Sign-up: submit values the app supplied at start (e.g. email/password) automatically. + // The full set is kept intact for the whole flow; only the attributes the server + // requests in this step are sent, so the app never sees them. + if flowType == .signUp, + let autoValues = autoAttributeValues(for: attributes, from: signUpAutofillValues) { + let autoIds = Set(autoValues.keys) + // If the server re-requests an attribute we already auto-submitted + // we throw an error specifying this + let repeats = autoIds.intersection(signUpAutofillSubmittedIds) + if !repeats.isEmpty { + let repeatedIds = repeats.sorted().joined(separator: ", ") + return failure( + .error(MSALNativeAuthFlowError( + kind: .generalError, + errorDescription: "The server re-requested attribute(s) already submitted: \(repeatedIds)." + )), + event: event, + context: context + ) + } + let submitState = makeState( + flowType, + continuationToken: token, + links: ["submitAttributes": submitHref], + username: username, + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds.union(autoIds) + ) + stopTelemetryEvent(event, context: context) + return await submitAttributes(autoValues, state: submitState) + } + let newState = makeState( + flowType, + continuationToken: token, + links: ["submitAttributes": submitHref], + username: username, + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds + ) stopTelemetryEvent(event, context: context) return response(.actionRequired( action: .attributesRequired(attributes: requiredAttributes(from: attributes)), @@ -743,7 +826,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa username: username, authMethods: authMethods, methodLinks: methodLinks, - scopes: scopes + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), context: context) @@ -756,7 +841,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa username: username, authMethods: authMethods, methodLinks: methodLinks, - scopes: scopes + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) return response(.actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), context: context) @@ -768,7 +855,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, - scopes: scopes + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo stopTelemetryEvent(event, context: context) @@ -899,7 +988,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa codeLength: Int? = nil, authMethods: [MSALAuthMethod] = [], methodLinks: [String: String] = [:], - scopes: [String] = [] + scopes: [String] = [], + signUpAutofillValues: [String: Any]? = nil, + signUpAutofillSubmittedIds: Set = [] ) -> MSALNativeAuthFlowState { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) var resolvedLinks: [String: URL] = [:] @@ -921,7 +1012,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa sentToHint: sentToHint, codeLength: codeLength, authMethods: authMethods, - scopes: scopes + scopes: scopes, + signUpAutofillValues: signUpAutofillValues, + signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) return MSALNativeAuthFlowState(continuation: continuation, controller: self) } @@ -963,6 +1056,26 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } + /// Returns the values needed to satisfy a `collectAttributes` request from the values the app + /// supplied at sign-up start, but only when *every* required attribute is covered. + /// Optional attributes are never auto-submitted + private func autoAttributeValues( + for requested: [MSALNativeAuthHALResponse.RequiredAttributeEntry], + from autofill: [String: Any]? + ) -> [String: Any]? { + guard let autofill = autofill, !requested.isEmpty else { + return nil + } + var values: [String: Any] = [:] + for entry in requested where entry.required { + guard let id = entry.id, let value = autofill[id] else { + return nil + } + values[id] = value + } + return values.isEmpty ? nil : values + } + // MARK: - Response construction private func response( diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift index 8955375456..9364e60dec 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -61,6 +61,16 @@ struct MSALNativeAuthV2ContinuationState { /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final /// `/token` exchange. Threaded through every step. let scopes: [String] + /// Values supplied by the app at sign-up start (keyed by attribute id, e.g. "email"/"password") + /// that the SDK submits automatically when the server issues a `collectAttributes` request for + /// them. Deliberately kept internal so the app never sees them again; must never be logged or + /// exposed on the public surface. + let signUpAutofillValues: [String: Any]? + /// Attribute ids already auto-submitted from ``signUpAutofillValues`` during this sign-up flow. + /// Used to detect when the server re-requests an attribute we already sent (e.g. after a + /// validation failure) so the SDK surfaces an error to the app instead of resending in a loop. + /// Carries no attribute values. + let signUpAutofillSubmittedIds: Set init( flowType: MSALNativeAuthV2FlowType, @@ -70,7 +80,9 @@ struct MSALNativeAuthV2ContinuationState { sentToHint: String? = nil, codeLength: Int? = nil, authMethods: [MSALAuthMethod] = [], - scopes: [String] = [] + scopes: [String] = [], + signUpAutofillValues: [String: Any]? = nil, + signUpAutofillSubmittedIds: Set = [] ) { self.flowType = flowType self.continuationToken = continuationToken @@ -80,6 +92,8 @@ struct MSALNativeAuthV2ContinuationState { self.codeLength = codeLength self.authMethods = authMethods self.scopes = scopes + self.signUpAutofillValues = signUpAutofillValues + self.signUpAutofillSubmittedIds = signUpAutofillSubmittedIds } func link(_ relation: String) -> URL? { diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index 1517abcaaf..efe8c1b0e2 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -300,6 +300,76 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { XCTAssertTrue(requestProviderMock.signUpStartCalled) } + func test_signUp_autoSubmitsSuppliedEmailAndPassword() async { + requestProviderMock.mockRequest() + let emailAttribute = MSALNativeAuthHALResponse.RequiredAttributeEntry(id: "email", type: "text", required: true, regex: nil) + let passwordAttribute = MSALNativeAuthHALResponse.RequiredAttributeEntry(id: "password", type: "password", required: true, regex: nil) + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup"), + .authorizationCode(code: "auth-code") + ] + validatorMock.interactionResponses = [ + .attributesRequired(continuationToken: "ct-email", attributes: [emailAttribute], submitHref: "https://contoso.com/submit"), + .codeRequired(continuationToken: "ct-code", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8), + .attributesRequired(continuationToken: "ct-pwd", attributes: [passwordAttribute], submitHref: "https://contoso.com/submit"), + .readyToComplete(continuationToken: "ct-continue") + ] + validatorMock.tokenResponse = .success(accessToken: "access-token") + + let parameters = MSALNativeAuthSignUpParameters(username: "user@contoso.com") + parameters.password = "Secret-Password-1" + + // Step 1: sign-up start. The server asks for `email`; the SDK auto-submits it and the app + // only sees the subsequent codeRequired action — never an attributesRequired for email. + let startResponse = await sut.signUp(parameters: parameters) + guard case .actionRequired(let startAction, let codeState) = startResponse.result, case .codeRequired = startAction else { + return XCTFail("Expected codeRequired action, got \(startResponse.result)") + } + + // Step 2: submit the email code. The server then asks for `password`; the SDK auto-submits + // the originally supplied password and completes without surfacing attributesRequired. + let finalResponse = await sut.submitCode("12345678", state: codeState) + guard case .completed = finalResponse.result else { + return XCTFail("Expected completed, got \(finalResponse.result)") + } + + // Email and password were submitted automatically as collected attributes. + XCTAssertTrue(requestProviderMock.submitAttributesCalled) + XCTAssertEqual(requestProviderMock.submitAttributesHistory.count, 2) + XCTAssertEqual(requestProviderMock.submitAttributesHistory.first?["email"] as? String, "user@contoso.com") + XCTAssertEqual(requestProviderMock.submitAttributesHistory.last?["password"] as? String, "Secret-Password-1") + XCTAssertTrue(requestProviderMock.verifyCalled) + XCTAssertTrue(requestProviderMock.tokenCalled) + } + + func test_signUp_whenServerRerequestsAlreadySubmittedAttribute_returnsError() async { + requestProviderMock.mockRequest() + let emailAttribute = MSALNativeAuthHALResponse.RequiredAttributeEntry(id: "email", type: "text", required: true, regex: nil) + validatorMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup") + ] + validatorMock.interactionResponses = [ + .attributesRequired(continuationToken: "ct-email", attributes: [emailAttribute], submitHref: "https://contoso.com/submit"), + .attributesRequired(continuationToken: "ct-email-2", attributes: [emailAttribute], submitHref: "https://contoso.com/submit") + ] + + let parameters = MSALNativeAuthSignUpParameters(username: "user@contoso.com") + parameters.password = "Secret-Password-1" + + let response = await sut.signUp(parameters: parameters) + + guard case .error(let error, _) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + // Email was auto-submitted exactly once; the re-request produced an error rather than a loop. + XCTAssertEqual(requestProviderMock.submitAttributesHistory.count, 1) + XCTAssertEqual(requestProviderMock.submitAttributesHistory.first?["email"] as? String, "user@contoso.com") + // The error names the repeated attribute id but never the value. + XCTAssertTrue(error.errorDescription?.contains("email") ?? false) + XCTAssertFalse(error.errorDescription?.contains("user@contoso.com") ?? true) + XCTAssertFalse(error.errorDescription?.contains("Secret-Password-1") ?? true) + } + func test_signIn_withPassword_happyPath_returnsCompleted() async { requestProviderMock.mockRequest() let passwordMethod = MSALNativeAuthHALResponse.EmbeddedMethod( diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index 611d5aa9e6..354802534d 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -51,6 +51,8 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { private(set) var submitPasswordHrefReceived: String? private(set) var submitCodeHrefReceived: String? private(set) var submitAttributesHrefReceived: String? + private(set) var submitAttributesReceived: [String: Any]? + private(set) var submitAttributesHistory: [[String: Any]] = [] private(set) var registerMethodHrefReceived: String? private(set) var updateHrefReceived: String? private(set) var pollHrefReceived: String? @@ -114,6 +116,8 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { submitAttributesCalled = true submitAttributesHrefReceived = href + submitAttributesReceived = attributes + submitAttributesHistory.append(attributes) return try resolveRequest() } From 8c380d440c66c30b18e3b09e21bb77c075319a17 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 16 Jul 2026 15:20:49 +0100 Subject: [PATCH 26/85] Merge origin/sedemche/native-auth-v2_release into origin/spetrescu/native-auth-v2 Link controller to public api --- CHANGELOG.md | 11 +- MSAL.podspec | 2 +- MSAL/MSAL.xcodeproj/project.pbxproj | 129 ++++- MSAL/resources/ios/Info.plist | 2 +- MSAL/resources/mac/Info.plist | 2 +- MSAL/src/MSALErrorConverter.m | 1 + MSAL/src/MSAL_Internal.h | 2 +- .../v2/MSALNativeAuthV2FlowController.swift | 228 ++++---- .../v2/MSALNativeAuthV2FlowControlling.swift | 4 +- .../v2/MSALNativeAuthV2FlowResult.swift | 145 +++++- .../errors/MSALNativeAuthErrorMessage.swift | 2 + ...SALNativeAuthV2HALResponseSerializer.swift | 6 +- .../MSALNativeAuthV2ResponseValidator.swift | 36 +- .../MSALNativeAuthV2ValidatedResponses.swift | 4 +- ...SALNativeAuthPublicClientApplication.swift | 9 +- ...SALNativeAuthResetPasswordParameters.swift | 5 - ...LNativeAuthResetPasswordParametersV2.swift | 39 ++ .../MSALNativeAuthSignUpParameters.swift | 5 - .../MSALNativeAuthSignUpParametersV2.swift | 39 ++ .../error/MFARequestChallengeError.swift | 14 +- .../error/MFASubmitChallengeError.swift | 18 +- .../error/MSALNativeAuthError.swift | 17 +- .../error/MSALNativeAuthGenericError.swift | 14 +- .../error/PasswordRequiredError.swift | 18 +- .../RegisterStrongAuthChallengeError.swift | 14 +- ...gisterStrongAuthSubmitChallengeError.swift | 14 +- .../error/ResetPasswordStartError.swift | 14 +- .../error/RetrieveAccessTokenError.swift | 14 +- .../error/SignInStartError.swift | 14 +- .../error/SignUpStartError.swift | 14 +- .../state_machine/error/VerifyCodeError.swift | 14 +- .../v2/MSALNativeAuthAction.swift | 22 +- .../v2/MSALNativeAuthFlowDelegate.swift | 59 +-- .../v2/MSALNativeAuthFlowError.swift | 177 +++++-- .../v2/MSALNativeAuthFlowScenario.swift | 50 ++ .../v2/MSALNativeAuthFlowState.swift | 75 +-- ...MSALNativeAuthAttributesInvalidState.swift | 72 +++ ...SALNativeAuthAttributesRequiredState.swift | 72 +++ .../MSALNativeAuthCodeRequiredState.swift | 89 ++++ .../MSALNativeAuthMFARequiredState.swift | 81 +++ ...tiveAuthMFAVerificationRequiredState.swift | 80 +++ ...ALNativeAuthNewPasswordRequiredState.swift | 64 +++ .../MSALNativeAuthPasswordRequiredState.swift | 64 +++ .../v2/state/MSALNativeAuthState.swift | 73 +++ ...hStrongAuthRegistrationRequiredState.swift | 82 +++ ...hStrongAuthVerificationRequiredState.swift | 80 +++ MSAL/src/public/MSALError.h | 5 + .../MSALTestAppAcquireTokenViewController.m | 4 +- MSAL/test/automation/tests/MSALBaseUITest.m | 118 +++++ .../tests/interactive/MSALChinaCloudUITests.m | 4 + .../MSALNativeAuthV2FlowControllerTests.swift | 34 +- .../MSALNativeAuthV2FlowControllerMock.swift | 2 +- ...SALNativeAuthV2ResponseValidatorMock.swift | 8 +- ...ALNativeAuthV2ResponseValidatorTests.swift | 28 +- Package.swift | 2 +- azure_pipelines/automation.yml | 244 +++++---- azure_pipelines/broker_submodule_check.yml | 209 ++------ azure_pipelines/pr-validation.yml | 488 ++++++++++-------- .../templates/tests-with-conf-file.yml | 66 ++- azure_pipelines/visionos-validation.yml | 8 +- build.py | 44 +- spm-integration-test.sh | 9 +- 62 files changed, 2317 insertions(+), 946 deletions(-) create mode 100644 MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParametersV2.swift create mode 100644 MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParametersV2.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthNewPasswordRequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthPasswordRequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift create mode 100644 MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 247fbfc6a1..3b28ffef31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ -TBD +## [2.13.0] * Update IdentityCore submodule to pull in DI foundation (common core #1810 WPJ, #1838 hardening, #1809 throttling) +* Migrating MSAL automation pipeline to ACES shared pool. + +## [2.12.1] +* Fix visionOS build failure: add visionos(2.0) availability annotation +* Remove sessionCorrelationId persistence code not required + +## [2.12.0] +* Add SECURITY.md with Microsoft standard security policy #2976 * Add Delos and GovSG sovereign cloud environments in `MSALAADAuthority`. (#2984) * Adding Get Device Token API for shared device mode #2980 * Improve UI tests performance #2981 @@ -9,7 +17,6 @@ TBD ## [2.11.0]: * Rename file on disk to match Xcode #2909 * Provide public api to set custom headers for CIAM requests #2862 -* Add SECURITY.md with Microsoft standard security policy #2976 ## [2.10.0]: * Add changes in podspec to support swift files added in common-core #2846 diff --git a/MSAL.podspec b/MSAL.podspec index ef8a6d7737..b6c6af91e3 100644 --- a/MSAL.podspec +++ b/MSAL.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "MSAL" - s.version = "2.11.0" + s.version = "2.13.0" s.summary = "Microsoft Authentication Library (MSAL) for iOS" s.description = <<-DESC The MSAL library for iOS gives your app the ability to begin using the Microsoft Cloud by supporting Microsoft Azure Active Directory and Microsoft Accounts in a converged experience using industry standard OAuth2 and OpenID Connect. The library also supports Microsoft Azure B2C for those using our hosted identity management service. diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 5e1e49d1af..0e9b2cb833 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -8,6 +8,32 @@ /* Begin PBXBuildFile section */ 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; + 01462653AC546A8B95A0D912 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; + 06AAD69B63B7A013959ECEF7 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; + 1FFBE815F16A5C07BCAEEA7A /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; + 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; + 3661378FB7B5DA5CCB37D76E /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; + 42E1FE910A9592561A2F44DC /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; + 4F6C95BC33A85725CB3F2185 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; + 5A7906E804836B39F0D61EE5 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; + 5DA9B72FCAECBF4718161CE1 /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; + 6534A6BDFED26846E71370A9 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; + 6A130FEA55D11486D2F4FA55 /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; + 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; + 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; + 79B0D18719E266EBFAA96F9D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; + 969B85F56D10B314FEE5E165 /* MSALNativeAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */; }; + 9959BEB45FADB738C8BFE331 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; + B311DA009515BDDE5FDF3679 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; + B3E12C5ECC553A95521CFEFA /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; + BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; + C4675E1CCC8208251CE74818 /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; + DE5CDB156AF38066359E4B66 /* MSALNativeAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */; }; + E03A45C678944B4F5D572922 /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; + E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + F05FC2CFEF1AE5462086AD0C /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; + F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; 04A6B5AE226936F30035C7C2 /* MSALFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F5BC91E59359900912CB8 /* MSALFramework.m */; }; 04A6B5AF226936F40035C7C2 /* MSALFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F5BC91E59359900912CB8 /* MSALFramework.m */; }; 04A6B5B0226936FE0035C7C2 /* MSIDVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = B2C17B091FC8DB2E0070A514 /* MSIDVersion.m */; }; @@ -420,7 +446,6 @@ 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; - 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; 7207E63A2FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -993,8 +1018,6 @@ B2FBB3DB28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FE601B20E5BB5800502BA6 /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; - BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; - BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; @@ -1137,14 +1160,18 @@ DE43150A2D3E551F009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */; }; DE43150B2D3E551F009A7FA2 /* MSALNativeAuthSignInParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */; }; DE43150C2D3E551F009A7FA2 /* MSALNativeAuthSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */; }; + FE0A0B00000000000000A002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */; }; DE43150D2D3E551F009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */; }; DE43150E2D3E551F009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */; }; + FE0A0B00000000000000A001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; DE43150F2D3E551F009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */; }; DE4315102D3E551F009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */; }; DE4315112D3E551F009A7FA2 /* MSALNativeAuthSignInParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */; }; DE4315122D3E551F009A7FA2 /* MSALNativeAuthSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */; }; + FE0A0B00000000000000B002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */; }; DE4315132D3E551F009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */; }; DE4315142D3E551F009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */; }; + FE0A0B00000000000000B001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; DE4315152D3E551F009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */; }; DE4F0F3129D6F1AA00D561FD /* MSALNativeAuthTokenIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4F0F2929D6F1AA00D561FD /* MSALNativeAuthTokenIntegrationTests.swift */; }; DE54B5912A434B9B00460B34 /* MSALNativeAuthTokenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */; }; @@ -1716,7 +1743,6 @@ E2F8900E2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */; }; E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; - E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; @@ -2263,6 +2289,19 @@ 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenRequestHandling.swift; sourceTree = ""; }; 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; + 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthRegistrationRequiredState.swift; sourceTree = ""; }; + 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFAVerificationRequiredState.swift; sourceTree = ""; }; + 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthNewPasswordRequiredState.swift; sourceTree = ""; }; + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowError.swift; sourceTree = ""; }; + 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowScenario.swift; sourceTree = ""; }; + 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthCodeRequiredState.swift; sourceTree = ""; }; + 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFARequiredState.swift; sourceTree = ""; }; + A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesRequiredState.swift; sourceTree = ""; }; + B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthPasswordRequiredState.swift; sourceTree = ""; }; + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowDelegate.swift; sourceTree = ""; }; + BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesInvalidState.swift; sourceTree = ""; }; + FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthVerificationRequiredState.swift; sourceTree = ""; }; + FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthState.swift; sourceTree = ""; }; 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 58B81F6524AC59A000E8799E /* MSALTestCacheTokenResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTestCacheTokenResponse.h; sourceTree = ""; }; 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTestCacheTokenResponse.m; sourceTree = ""; }; @@ -2283,7 +2322,6 @@ 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenResult.m; sourceTree = ""; }; 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControllerTests.swift; sourceTree = ""; }; 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowResult.swift; sourceTree = ""; }; - 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowError.swift; sourceTree = ""; }; 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCIAMAuthority.h; sourceTree = ""; }; 886F516329CCA58900F09471 /* MSALCIAMAuthority.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthority.m; sourceTree = ""; }; 88A25ED229E7185B00066311 /* MSALCIAMAuthorityTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthorityTests.m; sourceTree = ""; }; @@ -2543,7 +2581,6 @@ B2F45744211E41C100818910 /* MSALB2CInteractiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALB2CInteractiveTests.m; sourceTree = ""; }; B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALWPJMetaData+Internal.h"; sourceTree = ""; }; B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControlling.swift; sourceTree = ""; }; - B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowDelegate.swift; sourceTree = ""; }; B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequestInterceptorBridge.swift; sourceTree = ""; }; C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowController.swift; sourceTree = ""; }; @@ -2674,10 +2711,12 @@ DE40A4D22A8F80C100928CEE /* MSALNativeAuthSignUpContinueResponseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpContinueResponseErrorTests.swift; sourceTree = ""; }; DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthGetAccessTokenParameters.swift; sourceTree = ""; }; DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordParameters.swift; sourceTree = ""; }; + FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordParametersV2.swift; sourceTree = ""; }; DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MSALNativeAuthSignInAfterResetPasswordParameters .swift"; sourceTree = ""; }; DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInAfterSignUpParameters.swift; sourceTree = ""; }; DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInParameters.swift; sourceTree = ""; }; DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpParameters.swift; sourceTree = ""; }; + FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpParametersV2.swift; sourceTree = ""; }; DE4F0F2929D6F1AA00D561FD /* MSALNativeAuthTokenIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenIntegrationTests.swift; sourceTree = ""; }; DE53C7D4293F9F5A00E5B2BB /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenController.swift; sourceTree = ""; }; @@ -3564,6 +3603,36 @@ path = v2; sourceTree = ""; }; + 58F36BCB7532CC3D9D60EE3A /* state */ = { + isa = PBXGroup; + children = ( + FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */, + 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */, + B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */, + 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */, + A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */, + BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */, + 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */, + 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */, + 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */, + FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */, + ); + path = state; + sourceTree = ""; + }; + F58967D39C050697CB65B47C /* v2 */ = { + isa = PBXGroup; + children = ( + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, + 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, + 58F36BCB7532CC3D9D60EE3A /* state */, + AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */, + 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */, + ); + path = v2; + sourceTree = ""; + }; 4E0AA7DA6DFC949218EEA54E /* v2 */ = { isa = PBXGroup; children = ( @@ -4605,10 +4674,12 @@ children = ( DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */, DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */, + FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */, DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */, DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */, DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */, DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */, + FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */, 28F8D2932D8C5EBC005084FA /* MSALNativeAuthChallengeAuthMethodParameters.swift */, ); path = parameters; @@ -5451,18 +5522,6 @@ path = v2; sourceTree = ""; }; - F58967D39C050697CB65B47C /* v2 */ = { - isa = PBXGroup; - children = ( - 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, - AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */, - B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, - 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */, - ); - name = v2; - path = v2; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -7125,6 +7184,19 @@ 28DCD0B229D7392400C4601E /* ResetPasswordStates.swift in Sources */, 23A68A7620F5386A0071E435 /* MSALAADAuthority.m in Sources */, 28DCD09A29D7192F00C4601E /* MSALNativeAuthError.swift in Sources */, + BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */, + BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */, + 06AAD69B63B7A013959ECEF7 /* MSALNativeAuthFlowScenario.swift in Sources */, + DE5CDB156AF38066359E4B66 /* MSALNativeAuthState.swift in Sources */, + 6A130FEA55D11486D2F4FA55 /* MSALNativeAuthCodeRequiredState.swift in Sources */, + 3661378FB7B5DA5CCB37D76E /* MSALNativeAuthPasswordRequiredState.swift in Sources */, + B3E12C5ECC553A95521CFEFA /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */, + 6534A6BDFED26846E71370A9 /* MSALNativeAuthAttributesRequiredState.swift in Sources */, + F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */, + B311DA009515BDDE5FDF3679 /* MSALNativeAuthMFARequiredState.swift in Sources */, + 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */, + 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */, + 42E1FE910A9592561A2F44DC /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */, 28FDC4A92A38C0D100E38BE1 /* SignInAfterSignUpError.swift in Sources */, 233E96FD22653EFC007FCE2A /* MSALTelemetryEventsObservingProxy.m in Sources */, DE0D65C229D30C38005798B1 /* MSALNativeAuthSignInInitiateOauth2ErrorCode.swift in Sources */, @@ -7317,6 +7389,7 @@ DE43150A2D3E551F009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift in Sources */, DE43150B2D3E551F009A7FA2 /* MSALNativeAuthSignInParameters.swift in Sources */, DE43150C2D3E551F009A7FA2 /* MSALNativeAuthSignUpParameters.swift in Sources */, + FE0A0B00000000000000A002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */, DE89740F2DA52BE800C67203 /* MSALNativeAuthJITIntrospectOauth2ErrorCode.swift in Sources */, DE8974102DA52BE800C67203 /* MSALNativeAuthJITContinueOauth2ErrorCode.swift in Sources */, DE8974112DA52BE800C67203 /* MSALNativeAuthJITChallengeResponseError.swift in Sources */, @@ -7325,15 +7398,14 @@ DE8974142DA52BE800C67203 /* MSALNativeAuthJITContinueResponseError.swift in Sources */, DE43150D2D3E551F009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift in Sources */, DE43150E2D3E551F009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift in Sources */, + FE0A0B00000000000000A001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */, DE43150F2D3E551F009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift in Sources */, 232D68D8223DB8C200594BBD /* MSALSilentTokenParameters.m in Sources */, DE729ECD2A1793A100A761D9 /* MSALNativeAuthChannelType.swift in Sources */, E2EFAD162A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift in Sources */, 285F58542C5BA33B00F4EFA4 /* MSALNativeAuthSignInIntrospectRequestParameters.swift in Sources */, 28DE70D629FAC16700EB75AA /* MSALNativeAuthSignInResponseValidator.swift in Sources */, - BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */, C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */, - BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */, 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowState.swift in Sources */, 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthV2FlowResult.swift in Sources */, 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */, @@ -7487,6 +7559,19 @@ DE8DC48E2C6621A300534E8F /* SignUpStates+Internal.swift in Sources */, DE8DC4C62C6621C500534E8F /* MSALNativeAuthSignInChallengeResponse.swift in Sources */, DE8DC47F2C6621A100534E8F /* MSALNativeAuthError.swift in Sources */, + 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */, + E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */, + 5A7906E804836B39F0D61EE5 /* MSALNativeAuthFlowScenario.swift in Sources */, + 969B85F56D10B314FEE5E165 /* MSALNativeAuthState.swift in Sources */, + 1FFBE815F16A5C07BCAEEA7A /* MSALNativeAuthCodeRequiredState.swift in Sources */, + C4675E1CCC8208251CE74818 /* MSALNativeAuthPasswordRequiredState.swift in Sources */, + E03A45C678944B4F5D572922 /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */, + 9959BEB45FADB738C8BFE331 /* MSALNativeAuthAttributesRequiredState.swift in Sources */, + 5DA9B72FCAECBF4718161CE1 /* MSALNativeAuthAttributesInvalidState.swift in Sources */, + 01462653AC546A8B95A0D912 /* MSALNativeAuthMFARequiredState.swift in Sources */, + F05FC2CFEF1AE5462086AD0C /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */, + 4F6C95BC33A85725CB3F2185 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */, + 79B0D18719E266EBFAA96F9D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */, DE8DC4A82C6621B100534E8F /* MSALNativeAuthUrlRequestSerializer.swift in Sources */, 232D616522485BA700260C42 /* MSALIndividualClaimRequestAdditionalInfo.m in Sources */, DE8DC4CD2C6621C700534E8F /* MSALNativeAuthResendCodeRequestResponse.swift in Sources */, @@ -7620,8 +7705,10 @@ DEEFCE552DB0FA4800237F5A /* MSALNativeAuthLogger.swift in Sources */, DE4315112D3E551F009A7FA2 /* MSALNativeAuthSignInParameters.swift in Sources */, DE4315122D3E551F009A7FA2 /* MSALNativeAuthSignUpParameters.swift in Sources */, + FE0A0B00000000000000B002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */, DE4315132D3E551F009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift in Sources */, DE4315142D3E551F009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift in Sources */, + FE0A0B00000000000000B001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */, DE4315152D3E551F009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift in Sources */, DE8DC4852C6621A300534E8F /* SignInAfterPreviousFlowBaseState+Internal.swift in Sources */, DE8DC49E2C6621AE00534E8F /* MSALNativeAuthResetPasswordRequestProvider.swift in Sources */, @@ -7639,9 +7726,7 @@ DE8DC4612C66219600534E8F /* SignUpResults.swift in Sources */, DE8DC4972C6621A600534E8F /* SignInAfterResetPasswordDelegate.swift in Sources */, DE8DC4512C66218900534E8F /* MSALNativeAuthInternalError.swift in Sources */, - 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */, E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */, - E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */, 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowState.swift in Sources */, 33A0542A5B652892314FD6C8 /* MSALNativeAuthV2FlowResult.swift in Sources */, E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */, diff --git a/MSAL/resources/ios/Info.plist b/MSAL/resources/ios/Info.plist index 0b1015ac35..85264673c2 100644 --- a/MSAL/resources/ios/Info.plist +++ b/MSAL/resources/ios/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.11.0 + 2.13.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/MSAL/resources/mac/Info.plist b/MSAL/resources/mac/Info.plist index 98119b7416..17200dfa26 100644 --- a/MSAL/resources/mac/Info.plist +++ b/MSAL/resources/mac/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.11.0 + 2.13.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/MSAL/src/MSALErrorConverter.m b/MSAL/src/MSALErrorConverter.m index 1ef3a02ace..c620576827 100644 --- a/MSAL/src/MSALErrorConverter.m +++ b/MSAL/src/MSALErrorConverter.m @@ -76,6 +76,7 @@ + (void)initialize @(MSIDErrorUINotSupportedInExtension): @(MSALInternalErrorUINotSupportedInExtension), @(MSIDErrorInsufficientDeviceStrength): @(MSALErrorInsufficientDeviceStrength), @(MSIDErrorMDMEnrollmentCompletedNeedsRetry): @(MSALErrorMDMEnrollmentCompletedNeedsRetry), + @(MSIDErrorInvalidASWebAuthenticationURL): @(MSALInternalErrorInvalidASWebAuthenticationURL), // Broker errors @(MSIDErrorBrokerResponseNotReceived): @(MSALInternalErrorBrokerResponseNotReceived), @(MSIDErrorBrokerNoResumeStateFound): @(MSALInternalErrorBrokerNoResumeStateFound), diff --git a/MSAL/src/MSAL_Internal.h b/MSAL/src/MSAL_Internal.h index 7235188deb..6feef962ef 100644 --- a/MSAL/src/MSAL_Internal.h +++ b/MSAL/src/MSAL_Internal.h @@ -26,7 +26,7 @@ //------------------------------------------------------------------------------ #define MSAL_VER_HIGH 2 -#define MSAL_VER_LOW 11 +#define MSAL_VER_LOW 13 #define MSAL_VER_PATCH 0 #define STR_HELPER(x) #x diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift index 3650e55845..88f6563aea 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift @@ -68,16 +68,16 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Entry points - func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { - let flowType: MSALNativeAuthV2FlowType = .signUp + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthV2FlowControllerResponse { + let flowScenario: MSALNativeAuthFlowScenario = .signUp let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpStart, context: context) let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + sign_up link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowType: flowType, context: context) + let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, context: context) guard case .continuationToken(let continuationToken, let signUpLink) = authorizationChallenge else { - return failure(authorizationChallenge, event: event, context: context) + return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) } let startResult = await performInteraction(context: context) { @@ -101,7 +101,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return await mapInteraction( startResult, - flowType: flowType, + flowScenario: flowScenario, username: parameters.username, scopes: scopes, event: event, @@ -112,7 +112,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // swiftlint:disable:next function_body_length func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { - let flowType: MSALNativeAuthV2FlowType = .signIn + let flowScenario: MSALNativeAuthFlowScenario = .signIn let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil ? .telemetryApiIdV2SignInWithPasswordStart @@ -121,9 +121,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + sign_in link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowType: flowType, context: context) + let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, context: context) guard case .continuationToken(let continuationToken, let signInLink) = authorizationChallenge else { - return failure(authorizationChallenge, event: event, context: context) + return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) } let startResult = await performInteraction(context: context) { @@ -155,12 +155,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure( .error( MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "No usable sign-in method returned" ) ), event: event, - context: context + context: context, scenario: flowScenario ) } challengeContinuationToken = token @@ -173,7 +173,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa default: return await mapInteraction( startResult, - flowType: flowType, + flowScenario: flowScenario, username: parameters.username, scopes: scopes, event: event, @@ -191,7 +191,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( verifyResult, - flowType: flowType, + flowScenario: flowScenario, username: parameters.username, scopes: scopes, event: event, @@ -201,7 +201,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return await mapInteraction( challengeResult, - flowType: flowType, + flowScenario: flowScenario, username: parameters.username, scopes: scopes, event: event, @@ -210,16 +210,16 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { - let flowType: MSALNativeAuthV2FlowType = .resetPassword + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthV2FlowControllerResponse { + let flowScenario: MSALNativeAuthFlowScenario = .passwordReset let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordStart, context: context) let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + reset_password link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowType: flowType, context: context) + let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, context: context) guard case .continuationToken(let continuationToken, let resetPasswordLink) = authorizationChallenge else { - return failure(authorizationChallenge, event: event, context: context) + return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) } let startResult = await performInteraction(context: context) { @@ -232,7 +232,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } guard case .challengeRequired(let challengeContinuationToken, let challengeHref, let hint) = startResult else { - return interactionFailure(startResult, event: event, context: context, newState: nil) + return interactionFailure(startResult, event: event, context: context, scenario: flowScenario, newState: nil) } let challengeResult = await performInteraction(context: context) { @@ -241,7 +241,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return handleCodeRequired( challengeResult, - flowType: flowType, + flowScenario: flowScenario, username: parameters.username, fallbackHint: hint, scopes: scopes, @@ -260,15 +260,15 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa guard let verifyHref = continuation.link("verify")?.absoluteString else { let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } - switch continuation.flowType { + switch continuation.flowScenario { case .signIn, .signUp: - let apiId: MSALNativeAuthTelemetryApiId = continuation.flowType == .signUp + let apiId: MSALNativeAuthTelemetryApiId = continuation.flowScenario == .signUp ? .telemetryApiIdV2SignUpSubmitCode : .telemetryApiIdV2SignInSubmitCode let event = makeAndStartTelemetryEvent(id: apiId, context: context) @@ -277,7 +277,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, event: event, @@ -286,7 +286,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa signUpAutofillValues: continuation.signUpAutofillValues, signUpAutofillSubmittedIds: continuation.signUpAutofillSubmittedIds ) - case .resetPassword: + case .passwordReset: let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) let result = await performInteraction(context: context) { try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) @@ -294,20 +294,27 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch result { case .updateRequired(let token, let updateHref): let newState = makeState( - continuation.flowType, + continuation.flowScenario, continuationToken: token, links: ["update": updateHref], username: continuation.username, scopes: continuation.scopes ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) + return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context, scenario: continuation.flowScenario) case .error(let error): // Recoverable: allow the app to retry with the same code-required state. - return interactionFailure(result, event: event, context: context, newState: error.isInvalidCode ? state : nil) + return interactionFailure(result, event: event, context: context, scenario: continuation.flowScenario, newState: error.isInvalidCode ? state : nil) default: - return interactionFailure(result, event: event, context: context, newState: nil) + return interactionFailure(result, event: event, context: context, scenario: continuation.flowScenario, newState: nil) } + case .unknown: + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unknown flow for verify link")), + event: event, + context: context, scenario: continuation.flowScenario + ) } } @@ -318,9 +325,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa guard let verifyHref = continuation.link("verify")?.absoluteString else { return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } @@ -334,7 +341,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, event: event, @@ -351,9 +358,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa guard let updateHref = continuation.link("update")?.absoluteString else { return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing update link")), + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing update link")), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } @@ -367,7 +374,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } guard case .pollInProgress(var pollToken, let pollHref) = updateResult else { - return interactionFailure(updateResult, event: event, context: context, newState: nil) + return interactionFailure(updateResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) } var completionToken: String? @@ -387,9 +394,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa pollToken = token continue case .error: - return interactionFailure(pollResult, event: event, context: context, newState: nil) + return interactionFailure(pollResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) default: - return interactionFailure(pollResult, event: event, context: context, newState: nil) + return interactionFailure(pollResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) } if completionToken != nil { @@ -401,17 +408,17 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure( .error( MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "Password reset did not complete in time" ) ), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } return await completeWithToken( - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, continuationToken: completionToken, username: continuation.username, scopes: continuation.scopes, @@ -427,9 +434,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa guard let submitHref = continuation.link("submitAttributes")?.absoluteString else { return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing submit-attributes link")), + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing submit-attributes link")), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } @@ -443,7 +450,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, event: event, @@ -470,12 +477,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure( .error( MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "Missing enroll link for selected method" ) ), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) @@ -489,7 +496,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, event: event, @@ -501,12 +508,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return failure( .error( MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "Missing challenge link for selected method" ) ), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) @@ -517,7 +524,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch result { case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( - continuation.flowType, + continuation.flowScenario, continuationToken: token, links: ["verify": verifyHref, "resend": resendHref], username: continuation.username, @@ -529,11 +536,11 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return response(.actionRequired( action: .mfaVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), newState: newState - ), context: context) + ), context: context, scenario: continuation.flowScenario) default: return await mapInteraction( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, event: event, @@ -560,9 +567,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa isActivation = false } else { return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing verify link")), + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } @@ -585,7 +592,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } return await mapInteraction( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, event: event, @@ -601,9 +608,9 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa guard let resendHref = continuation.link("resend")?.absoluteString else { return failure( - .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing resend link")), + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing resend link")), event: event, - context: context + context: context, scenario: continuation.flowScenario ) } @@ -613,7 +620,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return handleCodeRequired( result, - flowType: continuation.flowType, + flowScenario: continuation.flowScenario, username: continuation.username, fallbackHint: continuation.sentToHint, scopes: continuation.scopes, @@ -625,24 +632,24 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Shared step helpers private func performAuthorizeChallengeStart( - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { let result: Result = await send { try self.requestProvider.authorizeChallengeStart(context: context) } - return responseValidator.validateAuthorizeChallenge(result, flowType: flowType) + return responseValidator.validateAuthorizeChallenge(result, flowScenario: flowScenario) } private func performAuthorizeChallengeContinue( - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { let result: Result = await send { try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, context: context) } - return responseValidator.validateAuthorizeChallenge(result, flowType: flowType) + return responseValidator.validateAuthorizeChallenge(result, flowScenario: flowScenario) } private func performInteraction( @@ -670,7 +677,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa private func handleCodeRequired( _ result: MSALNativeAuthV2InteractionValidatedResponse, - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, username: String?, fallbackHint: String?, scopes: [String], @@ -680,7 +687,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch result { case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["verify": verifyHref, "resend": resendHref], username: username, @@ -695,10 +702,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa action: .codeRequired(sentTo: displaySentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), newState: newState ), - context: context + context: context, scenario: flowScenario ) default: - return interactionFailure(result, event: event, context: context, newState: nil) + return interactionFailure(result, event: event, context: context, scenario: flowScenario, newState: nil) } } @@ -708,7 +715,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // swiftlint:disable:next function_body_length cyclomatic_complexity private func mapInteraction( _ result: MSALNativeAuthV2InteractionValidatedResponse, - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, username: String?, scopes: [String], event: MSIDTelemetryAPIEvent?, @@ -721,7 +728,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa switch result { case .readyToComplete(let token): return await completeWithToken( - flowType: flowType, + flowScenario: flowScenario, continuationToken: token, username: username, scopes: scopes, @@ -730,7 +737,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["verify": verifyHref, "resend": resendHref], username: username, @@ -745,10 +752,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return response(.actionRequired( action: .codeRequired(sentTo: displaySentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), newState: newState - ), context: context) + ), context: context, scenario: flowScenario) case .passwordRequired(let token, let verifyHref): let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["verify": verifyHref], username: username, @@ -757,10 +764,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .passwordRequired, newState: newState), context: context) + return response(.actionRequired(action: .passwordRequired, newState: newState), context: context, scenario: flowScenario) case .updateRequired(let token, let updateHref): let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["update": updateHref], username: username, @@ -769,12 +776,12 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context) + return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context, scenario: flowScenario) case .attributesRequired(let token, let attributes, let submitHref): // Sign-up: submit values the app supplied at start (e.g. email/password) automatically. // The full set is kept intact for the whole flow; only the attributes the server // requests in this step are sent, so the app never sees them. - if flowType == .signUp, + if flowScenario == .signUp, let autoValues = autoAttributeValues(for: attributes, from: signUpAutofillValues) { let autoIds = Set(autoValues.keys) // If the server re-requests an attribute we already auto-submitted @@ -784,15 +791,15 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let repeatedIds = repeats.sorted().joined(separator: ", ") return failure( .error(MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "The server re-requested attribute(s) already submitted: \(repeatedIds)." )), event: event, - context: context + context: context, scenario: flowScenario ) } let submitState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["submitAttributes": submitHref], username: username, @@ -804,7 +811,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return await submitAttributes(autoValues, state: submitState) } let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["submitAttributes": submitHref], username: username, @@ -816,11 +823,11 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa return response(.actionRequired( action: .attributesRequired(attributes: requiredAttributes(from: attributes)), newState: newState - ), context: context) + ), context: context, scenario: flowScenario) case .mfaRequired(let token, let methods, let challengeHref): let (authMethods, methodLinks) = authMethods(from: methods) let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["challenge": challengeHref], username: username, @@ -831,11 +838,11 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), context: context) + return response(.actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), context: context, scenario: flowScenario) case .registrationRequired(let token, let enrollHref, let methods): let (authMethods, methodLinks) = authMethods(from: methods) let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["enroll": enrollHref], username: username, @@ -846,10 +853,10 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), context: context) + return response(.actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), context: context, scenario: flowScenario) case .activationRequired(let token, let activateHref, let sentTo, let codeLength): let newState = makeState( - flowType, + flowScenario, continuationToken: token, links: ["activate": activateHref], username: username, @@ -868,15 +875,15 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa codeLength: codeLength ), newState: newState - ), context: context) + ), context: context, scenario: flowScenario) case .error(let error): stopTelemetryEvent(event, context: context, error: error) return response( - .error(error: error, newState: error.isInvalidCode || error.kind == .invalidPassword ? recoverableState : nil), - context: context + .error(error: error, newState: error.isInvalidCode || error.type == .invalidPassword ? recoverableState : nil), + context: context, scenario: flowScenario ) default: - return interactionFailure(result, event: event, context: context, newState: nil) + return interactionFailure(result, event: event, context: context, scenario: flowScenario, newState: nil) } } @@ -884,16 +891,16 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa /// The `/token` response is persisted to the shared MSAL token cache so the returned /// ``MSALNativeAuthUserAccountResult`` can retrieve access tokens via `getAccessToken(...)`. private func completeWithToken( - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, username: String?, scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2FlowControllerResponse { - let codeResult = await performAuthorizeChallengeContinue(flowType: flowType, continuationToken: continuationToken, context: context) + let codeResult = await performAuthorizeChallengeContinue(flowScenario: flowScenario, continuationToken: continuationToken, context: context) guard case .authorizationCode(let code) = codeResult else { - return failure(codeResult, event: event, context: context) + return failure(codeResult, event: event, context: context, scenario: flowScenario) } let tokenResponseResult = await performTokenExchange(code: code, scopes: scopes, context: context) @@ -904,22 +911,22 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa let tokenResult = try cacheTokenResponse(tokenResponse, context: context, msidConfiguration: msidConfiguration) guard let accountResult = resultFactory.makeUserAccountResult(tokenResult: tokenResult, context: context) else { - let error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to construct account result") + let error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unable to construct account result") stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) + return response(.error(error: error, newState: nil), context: context, scenario: flowScenario) } stopTelemetryEvent(event, context: context) - return response(.completed(accountResult), context: context) + return response(.completed(accountResult), context: context, scenario: flowScenario) } catch { - let flowError = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unable to save tokens to the cache") + let flowError = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unable to save tokens to the cache") stopTelemetryEvent(event, context: context, error: flowError) - return response(.error(error: flowError, newState: nil), context: context) + return response(.error(error: flowError, newState: nil), context: context, scenario: flowScenario) } case .failure(let error): let flowError = (error as? MSALNativeAuthFlowError) - ?? MSALNativeAuthFlowError(kind: .generalError, errorDescription: (error as NSError).localizedDescription) + ?? MSALNativeAuthFlowError(type: .generalError, errorDescription: (error as NSError).localizedDescription) stopTelemetryEvent(event, context: context, error: flowError) - return response(.error(error: flowError, newState: nil), context: context) + return response(.error(error: flowError, newState: nil), context: context, scenario: flowScenario) } } @@ -980,7 +987,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } private func makeState( - _ flowType: MSALNativeAuthV2FlowType, + _ flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, links: [String: String?], username: String?, @@ -1005,7 +1012,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } let continuation = MSALNativeAuthV2ContinuationState( - flowType: flowType, + flowScenario: flowScenario, continuationToken: continuationToken, links: resolvedLinks, username: username, @@ -1080,39 +1087,46 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa private func response( _ result: MSALNativeAuthV2FlowResult, - context: MSALNativeAuthRequestContext + context: MSALNativeAuthRequestContext, + scenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2FlowControllerResponse { - return MSALNativeAuthV2FlowControllerResponse(result, correlationId: context.correlationId()) + return MSALNativeAuthV2FlowControllerResponse( + result, + correlationId: context.correlationId(), + scenario: scenario + ) } private func failure( _ validated: MSALNativeAuthV2AuthorizeChallengeValidatedResponse, event: MSIDTelemetryAPIEvent?, - context: MSALNativeAuthRequestContext + context: MSALNativeAuthRequestContext, + scenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2FlowControllerResponse { let error: MSALNativeAuthFlowError if case .error(let flowError) = validated { error = flowError } else { - error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected authorize-challenge response") + error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected authorize-challenge response") } stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context) + return response(.error(error: error, newState: nil), context: context, scenario: scenario) } private func interactionFailure( _ validated: MSALNativeAuthV2InteractionValidatedResponse, event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, + scenario: MSALNativeAuthFlowScenario, newState: MSALNativeAuthFlowState? ) -> MSALNativeAuthV2FlowControllerResponse { let error: MSALNativeAuthFlowError if case .error(let flowError) = validated { error = flowError } else { - error = MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected server response") + error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected server response") } stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: newState), context: context) + return response(.error(error: error, newState: newState), context: context, scenario: scenario) } } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift index ce6c3b8a58..4fdab7c516 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift @@ -33,9 +33,9 @@ protocol MSALNativeAuthV2FlowControlling { // MARK: - Entry points - func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthV2FlowControllerResponse - func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthV2FlowControllerResponse func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift index 9364e60dec..e819e38b76 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift @@ -25,10 +25,10 @@ import Foundation /// Identifies which V2 flow a ``MSALNativeAuthFlowState`` belongs to. -enum MSALNativeAuthV2FlowType: CaseIterable { - case signUp - case signIn - case resetPassword +extension MSALNativeAuthFlowScenario { + + /// The server-driven flows the SDK follows when resolving `authorize-challenge` links. + static let authorizeChallengeFlows: [MSALNativeAuthFlowScenario] = [.signUp, .signIn, .passwordReset, .unknown] /// The `authorize-challenge` link relation this flow follows. var link: String { @@ -37,8 +37,10 @@ enum MSALNativeAuthV2FlowType: CaseIterable { return "sign_up" case .signIn: return "sign_in" - case .resetPassword: + case .passwordReset: return "reset_password" + case .unknown: + return "unknown" } } } @@ -48,7 +50,7 @@ enum MSALNativeAuthV2FlowType: CaseIterable { /// Holds the opaque server `continuation_token` and the resolved `_links` hrefs /// the SDK must follow to advance the server-driven flow. struct MSALNativeAuthV2ContinuationState { - let flowType: MSALNativeAuthV2FlowType + let flowScenario: MSALNativeAuthFlowScenario let continuationToken: String /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", /// "challenge", "enroll", "activate", "submitAttributes"). Per-method links are keyed "method:". @@ -73,7 +75,7 @@ struct MSALNativeAuthV2ContinuationState { let signUpAutofillSubmittedIds: Set init( - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, links: [String: URL], username: String?, @@ -84,7 +86,7 @@ struct MSALNativeAuthV2ContinuationState { signUpAutofillValues: [String: Any]? = nil, signUpAutofillSubmittedIds: Set = [] ) { - self.flowType = flowType + self.flowScenario = flowScenario self.continuationToken = continuationToken self.links = links self.username = username @@ -118,35 +120,146 @@ enum MSALNativeAuthV2FlowResult { struct MSALNativeAuthV2FlowControllerResponse { let result: MSALNativeAuthV2FlowResult let correlationId: UUID + /// The public scenario reported to the app (defaults to `.unknown` when the flow is undetermined). + let scenario: MSALNativeAuthFlowScenario let telemetryUpdate: ((Result) -> Void)? init( _ result: MSALNativeAuthV2FlowResult, correlationId: UUID, + scenario: MSALNativeAuthFlowScenario = .unknown, telemetryUpdate: ((Result) -> Void)? = nil ) { self.result = result self.correlationId = correlationId + self.scenario = scenario self.telemetryUpdate = telemetryUpdate } } /// Routes a controller response to the appropriate ``MSALNativeAuthFlowDelegate`` callback. +/// +/// V2 uses opt-in, per-state delegate protocols that extend ``MSALNativeAuthFlowDelegate``. For an +/// `actionRequired` result the dispatcher builds the concrete ``MSALNativeAuthState`` for the step, +/// wires it to the flow engine, and — if the app's delegate conforms to that step's protocol — +/// invokes its dedicated callback. If the app does not conform, the terminal +/// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. struct MSALNativeAuthFlowResponseDispatcher { + @MainActor func dispatch(_ response: MSALNativeAuthV2FlowControllerResponse, delegate: MSALNativeAuthFlowDelegate) async { + let scenario = response.scenario switch response.result { - case .actionRequired(let action, let newState): - await delegate.onActionRequired(action: action, flowState: newState) - response.telemetryUpdate?(.success(())) + case .actionRequired(let action, let engine): + await dispatchAction(action, engine: engine, scenario: scenario, response: response, delegate: delegate) case .completed(let result): - await delegate.onFlowCompleted(result: result) + await delegate.onFlowCompleted(result: result, scenario: scenario) + response.telemetryUpdate?(.success(())) + case .error(let error, _): + await delegate.onFlowError(error: error, scenario: scenario) + case .browserRequired(let url, _): + let error = MSALNativeAuthFlowError( + type: .browserRequired, + errorDescription: "The flow must continue in a web browser: \(url.absoluteString)", + correlationId: response.correlationId + ) + await delegate.onFlowError(error: error, scenario: scenario) response.telemetryUpdate?(.success(())) - case .error(let error, let newState): - await delegate.onFlowError(error: error, flowState: newState) - case .browserRequired(let url, let newState): - await delegate.onBrowserRequired(url: url, flowState: newState) + } + } + + @MainActor + private func dispatchAction( + _ action: MSALNativeAuthAction, + engine: MSALNativeAuthFlowState, + scenario: MSALNativeAuthFlowScenario, + response: MSALNativeAuthV2FlowControllerResponse, + delegate: MSALNativeAuthFlowDelegate + ) async { + switch action { + case .codeRequired(let sentTo, let channel, let codeLength): + let state = MSALNativeAuthCodeRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthCodeRequiredDelegate.self) { await $0.onCodeRequired(state: state, scenario: scenario) } + case .passwordRequired: + let state = MSALNativeAuthPasswordRequiredState() + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthPasswordRequiredDelegate.self) { await $0.onPasswordRequired(state: state, scenario: scenario) } + case .newPasswordRequired: + let state = MSALNativeAuthNewPasswordRequiredState() + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthNewPasswordRequiredDelegate.self) { await $0.onNewPasswordRequired(state: state, scenario: scenario) } + case .attributesRequired(let attributes): + let state = MSALNativeAuthAttributesRequiredState(attributes: attributes) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthAttributesRequiredDelegate.self) { + await $0.onAttributesRequired(state: state, scenario: scenario) + } + case .attributesInvalid(let attributeNames): + let state = MSALNativeAuthAttributesInvalidState(attributeNames: attributeNames) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthAttributesInvalidDelegate.self) { await $0.onAttributesInvalid(state: state, scenario: scenario) } + case .mfaRequired(let authMethods): + let state = MSALNativeAuthMFARequiredState(authMethods: authMethods) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthMFARequiredDelegate.self) { await $0.onMFARequired(state: state, scenario: scenario) } + case .mfaVerificationRequired(let sentTo, let channel, let codeLength): + let state = MSALNativeAuthMFAVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthMFAVerificationRequiredDelegate.self) { + await $0.onMFAVerificationRequired(state: state, scenario: scenario) + } + case .strongAuthRegistrationRequired(let authMethods): + let state = MSALNativeAuthStrongAuthRegistrationRequiredState(authMethods: authMethods) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthStrongAuthRegistrationRequiredDelegate.self) { + await $0.onStrongAuthRegistrationRequired(state: state, scenario: scenario) + } + case .strongAuthVerificationRequired(let sentTo, let channel, let codeLength): + let state = MSALNativeAuthStrongAuthVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) + await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthStrongAuthVerificationRequiredDelegate.self) { + await $0.onStrongAuthVerificationRequired(state: state, scenario: scenario) + } + } + } + + /// Wires a concrete state to the engine and invokes the app's per-state callback when the delegate + /// conforms to `Delegate`; otherwise reports `notImplemented` through the terminal error callback. + @MainActor + private func deliver( + _ state: MSALNativeAuthState, + engine: MSALNativeAuthFlowState, + scenario: MSALNativeAuthFlowScenario, + response: MSALNativeAuthV2FlowControllerResponse, + delegate: MSALNativeAuthFlowDelegate, + as delegateType: Delegate.Type, + callback: (Delegate) async -> Void + ) async { + prepare(state, engine: engine, scenario: scenario) + if let typedDelegate = delegate as? Delegate { + await callback(typedDelegate) response.telemetryUpdate?(.success(())) + } else { + await notImplemented(delegate: delegate, scenario: scenario, correlationId: response.correlationId) } } + + /// Wires a freshly built concrete state to the flow engine and stamps the originating scenario. + private func prepare(_ state: MSALNativeAuthState, engine: MSALNativeAuthFlowState, scenario: MSALNativeAuthFlowScenario) { + state.scenario = scenario + state.engine = engine + } + + @MainActor + private func notImplemented( + delegate: MSALNativeAuthFlowDelegate, + scenario: MSALNativeAuthFlowScenario, + correlationId: UUID + ) async { + await delegate.onFlowError( + error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: correlationId), + scenario: scenario + ) + } } diff --git a/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift b/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift index 1efa01d2a9..ceb577e229 100644 --- a/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift +++ b/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift @@ -39,6 +39,8 @@ enum MSALNativeAuthErrorMessage { static let invalidUsername = "Invalid username" static let generalError = "General error" static let invalidCode = "Invalid code" + static let delegateNotImplementedV2 = "Delegate %@ is not implemented" + static let invalidContinuationToken = "Invalid continuation token" static let invalidChallenge = "Invalid challenge" static let invalidInput = "Invalid input" static let refreshTokenExpired = "Refresh token is expired" diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 17c4df4444..fc67508667 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -95,9 +95,9 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali } } - for flowType in MSALNativeAuthV2FlowType.allCases where result[flowType.link] == nil { - if let href = json[flowType.link] as? String { - result[flowType.link] = href + for flowScenario in MSALNativeAuthFlowScenario.authorizeChallengeFlows where result[flowScenario.link] == nil { + if let href = json[flowScenario.link] as? String { + result[flowScenario.link] = href } } return result diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 8d1d21a2e6..0adb9767b9 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -28,7 +28,7 @@ import Foundation protocol MSALNativeAuthV2ResponseValidating { func validateAuthorizeChallenge( _ result: Result, - flowType: MSALNativeAuthV2FlowType + flowScenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse @@ -38,7 +38,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin func validateAuthorizeChallenge( _ result: Result, - flowType: MSALNativeAuthV2FlowType + flowScenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { switch result { case .failure(let error): @@ -51,17 +51,17 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .authorizationCode(code: code) } if let continuationToken = response.continuationToken { - let relation = flowType.link + let relation = flowScenario.link guard let href = response.links[relation] else { return .error(MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "Invalid authorize-challenge response: missing '\(relation)' link" )) } return .continuationToken(continuationToken: continuationToken, href: href) } return .error(MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "authorize-challenge returned neither a continuation token nor a code" )) } @@ -81,13 +81,13 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin if response.state == "continue" { guard let continuationToken = response.continuationToken else { - return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing continuation token in 'continue' response")) + return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in 'continue' response")) } return .readyToComplete(continuationToken: continuationToken) } guard let continuationToken = response.continuationToken else { - return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Missing continuation token in interaction response")) + return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in interaction response")) } // Sign-in method discovery: no action, but the available methods are embedded. @@ -184,7 +184,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin pollHref: pollHref ) default: - return .error(MSALNativeAuthFlowError(kind: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) + return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) } } } @@ -209,7 +209,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin /// Fail here rather than passing a missing href down to the next request. private func missingLink(_ relation: String) -> MSALNativeAuthV2InteractionValidatedResponse { return .error(MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "Invalid interaction response: missing '\(relation)' link" )) } @@ -217,30 +217,30 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { let message = serverError.message let errorCodes = estsErrorCodes(from: message) - let kind: MSALNativeAuthFlowError.Kind + let type: MSALNativeAuthFlowError.ErrorType if serverError.innerErrorCode == "invalidContinuationToken" { // An invalid OTP and an invalid continuation token share the inner code; the outer // code disambiguates (invalidGrant => the supplied OTP was wrong). - kind = serverError.code == "invalidGrant" ? .invalidCode : .invalidContinuationToken + type = serverError.code == "invalidGrant" ? .invalidCode : .invalidContinuationToken } else if let message = message, message.contains("AADSTS50034") { - kind = .userNotFound + type = .userNotFound } else if serverError.innerErrorCode == "invalidUserNameOrPassword" || errorCodes.contains(MSALNativeAuthESTSApiErrorCodes.invalidCredentials.rawValue) { // Wrong username/password at sign in (AADSTS50126): a recoverable credentials error, // not an invalid one-time code. - kind = .invalidPassword + type = .invalidPassword } else if serverError.code == "invalidGrant" { - kind = .invalidCode + type = .invalidCode } else { - kind = .generalError + type = .generalError } return MSALNativeAuthFlowError( - kind: kind, + type: type, errorDescription: message, errorCodes: errorCodes, - correlationId: serverError.correlationId + correlationId: serverError.correlationId ?? UUID() ) } @@ -272,7 +272,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return flowError } return MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: (error as NSError).localizedDescription ) } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index fc1b66461b..b67648c8b4 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -40,7 +40,7 @@ enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { case let (.authorizationCode(lCode), .authorizationCode(rCode)): return lCode == rCode case let (.error(lError), .error(rError)): - return lError.kind == rError.kind + return lError.type == rError.type default: return false } @@ -103,7 +103,7 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { case let (.readyToComplete(lToken), .readyToComplete(rToken)): return lToken == rToken case let (.error(lError), .error(rError)): - return lError.kind == rError.kind + return lError.type == rError.type default: return false } diff --git a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index 129685a4d5..1997e44309 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -265,11 +265,12 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic // MARK: - Native Auth V2 (server-driven) /// Sign up a user using the server-driven (V2) flow. + /// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. /// - Parameters: /// - parameters: Parameters used for the Sign Up flow. /// - delegate: Unified delegate that receives callbacks for the flow. public func signUpV2( - parameters: MSALNativeAuthSignUpParameters, + parameters: MSALNativeAuthSignUpParametersV2, delegate: MSALNativeAuthFlowDelegate ) { Task { @@ -282,6 +283,8 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic } /// Sign in a user using the server-driven (V2) flow. + /// + /// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. /// - Parameters: /// - parameters: Parameters used for the Sign In flow. /// - delegate: Unified delegate that receives callbacks for the flow. @@ -299,11 +302,13 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic } /// Reset the password using the server-driven (V2) flow. + /// + /// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. /// - Parameters: /// - parameters: Parameters used for the Reset Password flow. /// - delegate: Unified delegate that receives callbacks for the flow. public func resetPasswordV2( - parameters: MSALNativeAuthResetPasswordParameters, + parameters: MSALNativeAuthResetPasswordParametersV2, delegate: MSALNativeAuthFlowDelegate ) { Task { diff --git a/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift b/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift index c17dc66fa2..aa1448c456 100644 --- a/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift +++ b/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParameters.swift @@ -29,11 +29,6 @@ public class MSALNativeAuthResetPasswordParameters: NSObject { /// username of the account to reset password. public var username: String - /// Permissions you want included in the access token received once the account is signed in - /// at the end of the reset password flow. - /// Not all scopes are guaranteed to be included in the access token returned. - public var scopes: [String]? - /// UUID to correlate this request with the server for debugging. public var correlationId: UUID? diff --git a/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParametersV2.swift b/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParametersV2.swift new file mode 100644 index 0000000000..45ee43a9b2 --- /dev/null +++ b/MSAL/src/native_auth/public/parameters/MSALNativeAuthResetPasswordParametersV2.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/// Encapsulates the parameters passed to the resetPasswordV2 method of MSALNativeAuthPublicClientApplication. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthResetPasswordParametersV2: MSALNativeAuthResetPasswordParameters { + + /// Permissions you want included in the access token received once the account is signed in + /// at the end of the reset password flow. + /// Not all scopes are guaranteed to be included in the access token returned. + public var scopes: [String]? + + public override init(username: String) { + super.init(username: username) + } +} diff --git a/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift b/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift index f0577230cc..786f68f25d 100644 --- a/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift +++ b/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParameters.swift @@ -35,11 +35,6 @@ public class MSALNativeAuthSignUpParameters: NSObject { /// user attributes to be used during account creation. public var attributes: [String: Any]? - /// Permissions you want included in the access token received once the account is signed in - /// at the end of the sign up flow. - /// Not all scopes are guaranteed to be included in the access token returned. - public var scopes: [String]? - /// UUID to correlate this request with the server for debugging. public var correlationId: UUID? diff --git a/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParametersV2.swift b/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParametersV2.swift new file mode 100644 index 0000000000..e856ad01fb --- /dev/null +++ b/MSAL/src/native_auth/public/parameters/MSALNativeAuthSignUpParametersV2.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/// Encapsulates the parameters passed to the signUpV2 method of MSALNativeAuthPublicClientApplication. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthSignUpParametersV2: MSALNativeAuthSignUpParameters { + + /// Permissions you want included in the access token received once the account is signed in + /// at the end of the sign up flow. + /// Not all scopes are guaranteed to be included in the access token returned. + public var scopes: [String]? + + public override init(username: String) { + super.init(username: username) + } +} diff --git a/MSAL/src/native_auth/public/state_machine/error/MFARequestChallengeError.swift b/MSAL/src/native_auth/public/state_machine/error/MFARequestChallengeError.swift index ffe4c2cec9..3c4a5c3bc6 100644 --- a/MSAL/src/native_auth/public/state_machine/error/MFARequestChallengeError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/MFARequestChallengeError.swift @@ -37,7 +37,14 @@ public class MFARequestChallengeError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -60,9 +67,4 @@ public class MFARequestChallengeError: MSALNativeAuthError { public var isAuthMethodBlocked: Bool { return type == .authMethodBlocked } - - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } } diff --git a/MSAL/src/native_auth/public/state_machine/error/MFASubmitChallengeError.swift b/MSAL/src/native_auth/public/state_machine/error/MFASubmitChallengeError.swift index 9de15bf7df..0aba4566b3 100644 --- a/MSAL/src/native_auth/public/state_machine/error/MFASubmitChallengeError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/MFASubmitChallengeError.swift @@ -37,7 +37,14 @@ public class MFASubmitChallengeError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } init(error: VerifyCodeError) { @@ -53,7 +60,9 @@ public class MFASubmitChallengeError: MSALNativeAuthError { message: error.errorDescription, correlationId: error.correlationId, errorCodes: error.errorCodes, - errorUri: error.errorUri + errorUri: error.errorUri, + isBrowserRequired: self.type == .browserRequired, + isGeneralError: self.type == .generalError ) } @@ -77,9 +86,4 @@ public class MFASubmitChallengeError: MSALNativeAuthError { public var isInvalidChallenge: Bool { return type == .invalidChallenge } - - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } } diff --git a/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthError.swift b/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthError.swift index 72a3d56edb..3c53c26a05 100644 --- a/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthError.swift @@ -39,12 +39,27 @@ public class MSALNativeAuthError: NSObject, LocalizedError { /// Error uri that can be followed to get more information about the error returned by the server public let errorUri: String? + /// Indicates whether the error can only be resolved by falling back to the browser-based interactive flow. + public let isBrowserRequired: Bool + + /// Indicates whether the error is an unclassified or unexpected error with no more specific handling. + public let isGeneralError: Bool + private let message: String? - init(message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { + init( + message: String? = nil, + correlationId: UUID, + errorCodes: [Int] = [], + errorUri: String? = nil, + isBrowserRequired: Bool = false, + isGeneralError: Bool = false + ) { self.message = message self.correlationId = correlationId self.errorCodes = errorCodes self.errorUri = errorUri + self.isBrowserRequired = isBrowserRequired + self.isGeneralError = isGeneralError } } diff --git a/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthGenericError.swift b/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthGenericError.swift index a44366e0f4..7e6d705bef 100644 --- a/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthGenericError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/MSALNativeAuthGenericError.swift @@ -36,7 +36,14 @@ public class MSALNativeAuthGenericError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -53,9 +60,4 @@ public class MSALNativeAuthGenericError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - } diff --git a/MSAL/src/native_auth/public/state_machine/error/PasswordRequiredError.swift b/MSAL/src/native_auth/public/state_machine/error/PasswordRequiredError.swift index 9e4dc75d48..9a3638e56a 100644 --- a/MSAL/src/native_auth/public/state_machine/error/PasswordRequiredError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/PasswordRequiredError.swift @@ -37,7 +37,14 @@ public class PasswordRequiredError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } init(signInStartError: SignInStartError) { @@ -58,7 +65,9 @@ public class PasswordRequiredError: MSALNativeAuthError { message: errorDescription, correlationId: signInStartError.correlationId, errorCodes: signInStartError.errorCodes, - errorUri: signInStartError.errorUri + errorUri: signInStartError.errorUri, + isBrowserRequired: self.type == .browserRequired, + isGeneralError: self.type == .generalError ) } @@ -78,11 +87,6 @@ public class PasswordRequiredError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` when the password is not valid. public var isInvalidPassword: Bool { return type == .invalidPassword diff --git a/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthChallengeError.swift b/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthChallengeError.swift index ad64890182..f459591eb1 100644 --- a/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthChallengeError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthChallengeError.swift @@ -37,7 +37,14 @@ public class RegisterStrongAuthChallengeError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -63,11 +70,6 @@ public class RegisterStrongAuthChallengeError: MSALNativeAuthError { return type == .invalidInput } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` when the verification contact provided has been blocked. Try using another email or phone number, or select an alternative authentication method. public var isVerificationContactBlocked: Bool { return type == .verificationContactBlocked diff --git a/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthSubmitChallengeError.swift b/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthSubmitChallengeError.swift index f3821827c8..a1ba1a7c90 100644 --- a/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthSubmitChallengeError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/RegisterStrongAuthSubmitChallengeError.swift @@ -36,7 +36,14 @@ public class RegisterStrongAuthSubmitChallengeError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -59,9 +66,4 @@ public class RegisterStrongAuthSubmitChallengeError: MSALNativeAuthError { public var isInvalidChallenge: Bool { return type == .invalidChallenge } - - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } } diff --git a/MSAL/src/native_auth/public/state_machine/error/ResetPasswordStartError.swift b/MSAL/src/native_auth/public/state_machine/error/ResetPasswordStartError.swift index d47464aa76..6ab465d68b 100644 --- a/MSAL/src/native_auth/public/state_machine/error/ResetPasswordStartError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/ResetPasswordStartError.swift @@ -39,7 +39,14 @@ public class ResetPasswordStartError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -62,11 +69,6 @@ public class ResetPasswordStartError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` if the user does not have a password. public var isUserDoesNotHavePassword: Bool { return type == .userDoesNotHavePassword diff --git a/MSAL/src/native_auth/public/state_machine/error/RetrieveAccessTokenError.swift b/MSAL/src/native_auth/public/state_machine/error/RetrieveAccessTokenError.swift index 70ed9eebf1..c42a7ed056 100644 --- a/MSAL/src/native_auth/public/state_machine/error/RetrieveAccessTokenError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/RetrieveAccessTokenError.swift @@ -38,7 +38,14 @@ public class RetrieveAccessTokenError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -59,11 +66,6 @@ public class RetrieveAccessTokenError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` if the refresh token has expired. public var isRefreshTokenExpired: Bool { return type == .refreshTokenExpired diff --git a/MSAL/src/native_auth/public/state_machine/error/SignInStartError.swift b/MSAL/src/native_auth/public/state_machine/error/SignInStartError.swift index cfec543b91..47e5eac48c 100644 --- a/MSAL/src/native_auth/public/state_machine/error/SignInStartError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/SignInStartError.swift @@ -39,7 +39,14 @@ public class SignInStartError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -62,11 +69,6 @@ public class SignInStartError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` if the user that is trying to sign in cannot be found. public var isUserNotFound: Bool { return type == .userNotFound diff --git a/MSAL/src/native_auth/public/state_machine/error/SignUpStartError.swift b/MSAL/src/native_auth/public/state_machine/error/SignUpStartError.swift index 0ef525d2dc..d3b7107d67 100644 --- a/MSAL/src/native_auth/public/state_machine/error/SignUpStartError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/SignUpStartError.swift @@ -39,7 +39,14 @@ public class SignUpStartError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -62,11 +69,6 @@ public class SignUpStartError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` when the user is trying to register an existing username. public var isUserAlreadyExists: Bool { return type == .userAlreadyExists diff --git a/MSAL/src/native_auth/public/state_machine/error/VerifyCodeError.swift b/MSAL/src/native_auth/public/state_machine/error/VerifyCodeError.swift index 9bbbf34ccd..d2d661f940 100644 --- a/MSAL/src/native_auth/public/state_machine/error/VerifyCodeError.swift +++ b/MSAL/src/native_auth/public/state_machine/error/VerifyCodeError.swift @@ -37,7 +37,14 @@ public class VerifyCodeError: MSALNativeAuthError { init(type: ErrorType, message: String? = nil, correlationId: UUID, errorCodes: [Int] = [], errorUri: String? = nil) { self.type = type - super.init(message: message, correlationId: correlationId, errorCodes: errorCodes, errorUri: errorUri) + super.init( + message: message, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } /// Describes why an error occurred and provides more information about the error. @@ -56,11 +63,6 @@ public class VerifyCodeError: MSALNativeAuthError { } } - /// Returns `true` if a browser is required to continue the operation. - public var isBrowserRequired: Bool { - return type == .browserRequired - } - /// Returns `true` when the code introduced is not valid. public var isInvalidCode: Bool { return type == .invalidCode diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift index 5c50ed9935..797d8ad0bc 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift @@ -26,42 +26,44 @@ import Foundation /// Actions the server can request during a Native Auth V2 (server-driven) flow. /// -/// In V2 the server drives the flow: each step the SDK reports an -/// ``MSALNativeAuthAction`` through ``MSALNativeAuthFlowDelegate/onActionRequired(action:flowState:)`` -/// and the app continues by calling the corresponding method on the supplied -/// ``MSALNativeAuthFlowState``. +/// In V2 the server drives the flow: at each step the SDK reports the concrete +/// ``MSALNativeAuthState`` for that step through its dedicated ``MSALNativeAuthFlowDelegate`` +/// callback, and the app continues by calling the corresponding method on that state. public enum MSALNativeAuthAction { /// The server requires the user to verify a one-time code. - /// Continue with ``MSALNativeAuthFlowState/submitCode(_:delegate:)``. + /// Continue with ``MSALNativeAuthCodeRequiredState/submitCode(_:delegate:)``. case codeRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) /// The server requires the user to enter their password. - /// Continue with ``MSALNativeAuthFlowState/submitPassword(_:delegate:)``. + /// Continue with ``MSALNativeAuthPasswordRequiredState/submitPassword(_:delegate:)``. case passwordRequired /// The server requires the user to enter a new password (self-service password reset). - /// Continue with ``MSALNativeAuthFlowState/submitNewPassword(_:delegate:)``. + /// Continue with ``MSALNativeAuthNewPasswordRequiredState/submitNewPassword(_:delegate:)``. case newPasswordRequired /// The server requires additional user attributes. - /// Continue with ``MSALNativeAuthFlowState/submitAttributes(_:delegate:)``. + /// Continue with ``MSALNativeAuthAttributesRequiredState/submitAttributes(_:delegate:)``. case attributesRequired(attributes: [MSALNativeAuthRequiredAttribute]) /// The server reports that some attributes were invalid and must be corrected. + /// Continue with ``MSALNativeAuthAttributesInvalidState/submitAttributes(_:delegate:)``. case attributesInvalid(attributeNames: [String]) /// The server requires multi-factor authentication; the user must select an auth method. - /// Continue with ``MSALNativeAuthFlowState/selectAuthMethod(_:verificationContact:delegate:)``. + /// Continue with ``MSALNativeAuthMFARequiredState/selectAuthMethod(_:verificationContact:delegate:)``. case mfaRequired(authMethods: [MSALAuthMethod]) /// The server sent an MFA challenge; the user must enter the verification code. - /// Continue with ``MSALNativeAuthFlowState/submitChallenge(_:delegate:)``. + /// Continue with ``MSALNativeAuthMFAVerificationRequiredState/submitChallenge(_:delegate:)``. case mfaVerificationRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) /// The server requires strong authentication registration (JIT); the user must select an auth method. + /// Continue with ``MSALNativeAuthStrongAuthRegistrationRequiredState/selectAuthMethod(_:verificationContact:delegate:)``. case strongAuthRegistrationRequired(authMethods: [MSALAuthMethod]) /// The server sent a JIT challenge; the user must enter the verification code. + /// Continue with ``MSALNativeAuthStrongAuthVerificationRequiredState/submitChallenge(_:delegate:)``. case strongAuthVerificationRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) } diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift index c6a45e92dd..7f8c6c9a88 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift @@ -24,48 +24,37 @@ import Foundation -/// Single unified delegate for all Native Auth V2 (server-driven) flows. +/// Shared base delegate for all Native Auth V2 (server-driven) flows. /// -/// One delegate serves sign up, sign in and reset password. The SDK drives the flow and +/// Unlike V1 — which exposes a different delegate protocol per step — V2 uses one +/// family of delegates for sign up, sign in and reset password. The SDK drives the flow and /// reports back through these callbacks; the app reacts and continues the flow by -/// calling methods on the provided ``MSALNativeAuthFlowState``. +/// calling methods directly on the ``MSALNativeAuthState`` it is handed. +/// +/// This base protocol declares only the two terminal callbacks (``onFlowCompleted(result:scenario:)`` +/// and ``onFlowError(error:scenario:)``) that every flow reports. Each server-driven state is +/// delivered through its own per-state delegate protocol (e.g. ``MSALNativeAuthCodeRequiredDelegate``) +/// that extends this base and adds a single strongly-typed, required callback. The app conforms to +/// the per-state protocols for the states it wants to handle; if it does not conform to a state's +/// protocol, ``onFlowError(error:scenario:)`` is called with error type `notImplemented`. /// /// All callbacks are invoked on the main actor. -public protocol MSALNativeAuthFlowDelegate: AnyObject { - - /// The server requires the user to perform an action before the flow can continue. - /// - Parameters: - /// - action: The action the server is requesting. - /// - flowState: Opaque handle used to continue the flow. - @MainActor func onActionRequired(action: MSALNativeAuthAction, flowState: MSALNativeAuthFlowState) +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthFlowDelegate { /// The flow completed successfully and the user now has tokens. - /// - Parameter result: The authenticated user account result. - @MainActor func onFlowCompleted(result: MSALNativeAuthUserAccountResult) - - /// The flow encountered an error. /// - Parameters: - /// - error: The error that occurred. - /// - flowState: Opaque handle used to retry/continue the flow, when the error is recoverable. - @MainActor func onFlowError(error: MSALNativeAuthFlowError, flowState: MSALNativeAuthFlowState?) + /// - result: The authenticated user account result. + /// - scenario: The flow that produced this callback. + @MainActor func onFlowCompleted(result: MSALNativeAuthUserAccountResult, scenario: MSALNativeAuthFlowScenario) - /// The server requires the flow to continue in a web browser (e.g. an unsupported scenario). + /// The flow encountered an error. /// - Parameters: - /// - url: The URL to open in a browser. - /// - flowState: Opaque handle used to continue the flow. - @MainActor func onBrowserRequired(url: URL, flowState: MSALNativeAuthFlowState) -} - -/// Default implementation makes ``onBrowserRequired(url:flowState:)`` optional. -public extension MSALNativeAuthFlowDelegate { - - @MainActor func onBrowserRequired(url: URL, flowState: MSALNativeAuthFlowState) { - onFlowError( - error: MSALNativeAuthFlowError( - kind: .browserRequired, - errorDescription: "The flow requires a web browser, but onBrowserRequired(url:flowState:) is not implemented." - ), - flowState: flowState - ) - } + /// - error: The error that occurred. The app decides whether it can retry by + /// inspecting the error (e.g. `error.isInvalidCode` / `error.isInvalidPassword`) and calling + /// the appropriate method again on the state it is currently handling. + /// - scenario: The flow that produced this callback. + @MainActor func onFlowError(error: MSALNativeAuthFlowError, scenario: MSALNativeAuthFlowScenario) } diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift index 2384354d84..a65c9e3e40 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift @@ -29,76 +29,185 @@ import Foundation /// A single error type is used across all V2 flows so that an app's /// ``MSALNativeAuthFlowDelegate`` has only one error shape to inspect, mirroring /// the unified delegate contract described in the V2 interface. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. @objcMembers -public class MSALNativeAuthFlowError: NSObject, LocalizedError { +public class MSALNativeAuthFlowError: MSALNativeAuthError { /// High level classification of the V2 flow error. - public enum Kind: Int { - /// The requested flow (or one of its steps) is not implemented yet. + enum ErrorType: CaseIterable { + /// The delegate is not implemented. case notImplemented - /// The provided username was not accepted by the server (e.g. AADSTS50034 user not found). + /// The provided username was not accepted by the server. case userNotFound /// The submitted one-time code was invalid or expired. case invalidCode - /// The continuation token was rejected by the server (wrong endpoint, tampered or expired). + /// The continuation token was rejected by the server. case invalidContinuationToken /// The submitted password did not meet the server's requirements. case invalidPassword + /// The username and/or password supplied at sign in were not accepted by the server. + case invalidCredentials /// The username supplied to the SDK failed local validation. case invalidUsername + /// The account does not have a password associated with it. + case userDoesNotHavePassword + /// An account already exists for the supplied username during sign up. + case userAlreadyExists + /// The submitted authentication challenge was rejected by the server. + case invalidChallenge + /// The server blocked the requested strong authentication method. + case authMethodBlocked + /// The server blocked the verification contact provided for strong authentication. + case verificationContactBlocked + /// The input supplied for a strong authentication registration step was invalid. + case invalidInput /// The flow must continue in a web browser. case browserRequired /// A generic / unexpected error occurred. case generalError } - /// The classification of this error. - public let kind: Kind - - /// A developer-facing description of the error. - public let errorDescription: String? - - /// Server error codes associated with this error, when available. - public let errorCodes: [Int] - - /// UUID correlating this error with the server logs, when available. - public let correlationId: UUID? + let type: ErrorType init( - kind: Kind, + type: ErrorType, errorDescription: String? = nil, errorCodes: [Int] = [], - correlationId: UUID? = nil + correlationId: UUID, + errorUri: String? = nil ) { - self.kind = kind - self.errorDescription = errorDescription - self.errorCodes = errorCodes - self.correlationId = correlationId + self.type = type + super.init( + message: errorDescription, + correlationId: correlationId, + errorCodes: errorCodes, + errorUri: errorUri, + isBrowserRequired: type == .browserRequired, + isGeneralError: type == .generalError + ) } - /// Whether the flow that produced this error is not implemented yet. - public var isNotImplemented: Bool { - return kind == .notImplemented + /// Convenience initializer for internal SDK-originated errors that have no server correlation id. + /// A fresh correlation id is generated so the error still carries one for diagnostics. + convenience init( + type: ErrorType, + errorDescription: String? = nil, + errorCodes: [Int] = [], + errorUri: String? = nil + ) { + self.init( + type: type, + errorDescription: errorDescription, + errorCodes: errorCodes, + correlationId: UUID(), + errorUri: errorUri + ) } - /// Whether the submitted one-time code was invalid. - public var isInvalidCode: Bool { - return kind == .invalidCode + /// Describes why an error occurred and provides more information about the error. + public override var errorDescription: String? { + if let description = super.errorDescription { + return description + } + + switch type { + case .notImplemented: + return MSALNativeAuthErrorMessage.delegateNotImplementedV2 + case .userNotFound: + return MSALNativeAuthErrorMessage.userNotFound + case .invalidCode: + return MSALNativeAuthErrorMessage.invalidCode + case .invalidContinuationToken: + return MSALNativeAuthErrorMessage.invalidContinuationToken + case .invalidPassword: + return MSALNativeAuthErrorMessage.invalidPassword + case .invalidCredentials: + return MSALNativeAuthErrorMessage.invalidCredentials + case .invalidUsername: + return MSALNativeAuthErrorMessage.invalidUsername + case .userDoesNotHavePassword: + return MSALNativeAuthErrorMessage.userDoesNotHavePassword + case .userAlreadyExists: + return MSALNativeAuthErrorMessage.userAlreadyExists + case .invalidChallenge: + return MSALNativeAuthErrorMessage.invalidChallenge + case .authMethodBlocked: + return MSALNativeAuthErrorMessage.authMethodBlocked + case .verificationContactBlocked: + return MSALNativeAuthErrorMessage.verificationContactBlocked + case .invalidInput: + return MSALNativeAuthErrorMessage.invalidInput + case .browserRequired: + return MSALNativeAuthErrorMessage.browserRequired + case .generalError: + return MSALNativeAuthErrorMessage.generalError + } } - /// Whether the submitted password was rejected (wrong credentials at sign in, or a password - /// that did not satisfy the server's policy during sign up). - public var isInvalidPassword: Bool { - return kind == .invalidPassword + /// Whether the delegate that produced this error is not implemented yet. + public var isNotImplemented: Bool { + return type == .notImplemented } /// Whether the username was not found in the directory. public var isUserNotFound: Bool { - return kind == .userNotFound + return type == .userNotFound + } + + /// Whether the submitted one-time code was invalid. + public var isInvalidCode: Bool { + return type == .invalidCode } /// Whether the continuation token was rejected by the server. public var isInvalidContinuationToken: Bool { - return kind == .invalidContinuationToken + return type == .invalidContinuationToken + } + + /// Whether the submitted password was rejected because it did not satisfy the server's + /// policy during sign up. + public var isInvalidPassword: Bool { + return type == .invalidPassword + } + + /// Whether the username and/or password supplied at sign in were not accepted by the server. + public var isInvalidCredentials: Bool { + return type == .invalidCredentials + } + + /// Whether the username supplied to the SDK failed local validation. + public var isInvalidUsername: Bool { + return type == .invalidUsername + } + + /// Whether the account does not have a password associated with it. + public var isUserDoesNotHavePassword: Bool { + return type == .userDoesNotHavePassword + } + + /// Whether an account already exists for the supplied username during sign up. + public var isUserAlreadyExists: Bool { + return type == .userAlreadyExists + } + + /// Whether the submitted authentication challenge was rejected by the server. + public var isInvalidChallenge: Bool { + return type == .invalidChallenge + } + + /// Whether the server blocked the requested strong authentication method. + public var isAuthMethodBlocked: Bool { + return type == .authMethodBlocked + } + + /// Whether the server blocked the verification contact provided for strong authentication. + public var isVerificationContactBlocked: Bool { + return type == .verificationContactBlocked + } + + /// Whether the input supplied for a strong authentication registration step was invalid. + public var isInvalidInput: Bool { + return type == .invalidInput } } diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift new file mode 100644 index 0000000000..b292eb85e4 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Identifies which Native Auth V2 (server-driven) flow a ``MSALNativeAuthFlowDelegate`` callback +/// belongs to. +/// +/// Because V2 uses a single unified delegate for sign in, sign up and password reset, every delegate +/// callback also reports the `scenario` that triggered it so the app can react appropriately without +/// tracking the originating flow itself. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public enum MSALNativeAuthFlowScenario: Int { + + /// The scenario could not be determined. This is the default value and should not normally be + /// reported to the app; it acts as a safe placeholder until a concrete flow scenario is resolved. + case unknown + + /// The callback originated from a sign up flow. + case signUp + + /// The callback originated from a sign in flow. + case signIn + + /// The callback originated from a password reset flow. + case passwordReset +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift index ea251a4abd..5dbafca5cf 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift @@ -24,12 +24,16 @@ import Foundation -/// Opaque handle that lets an app continue a Native Auth V2 (server-driven) flow. +/// Internal engine that continues a Native Auth V2 (server-driven) flow. /// -/// The SDK hands a ``MSALNativeAuthFlowState`` to the app via -/// ``MSALNativeAuthFlowDelegate/onActionRequired(action:flowState:)``. The app then -/// calls the method matching the requested ``MSALNativeAuthAction`` to advance the flow. -public class MSALNativeAuthFlowState { +/// The SDK creates one engine per flow and hands it to each concrete ``MSALNativeAuthState`` it +/// produces (via the dispatcher). The concrete state's public continuation methods (e.g. +/// `submitCode(_:delegate:)`) forward to ``run(delegate:operation:)``, which invokes the matching +/// controller operation and routes the resulting response back through the dispatcher. +/// +/// This type carries no public API surface — apps interact only with the concrete +/// ``MSALNativeAuthState`` subclasses. +class MSALNativeAuthFlowState { let continuation: MSALNativeAuthV2ContinuationState private let controller: MSALNativeAuthV2FlowControlling @@ -40,65 +44,14 @@ public class MSALNativeAuthFlowState { self.controller = controller } - /// Submit a one-time verification code. - public func submitCode(_ code: String, delegate: MSALNativeAuthFlowDelegate) { - run(delegate: delegate) { controller in - await controller.submitCode(code, state: self) - } - } - - /// Submit a password (sign in / sign up). - public func submitPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { - run(delegate: delegate) { controller in - await controller.submitPassword(password, state: self) - } - } - - /// Submit a new password (self-service password reset). - public func submitNewPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { - run(delegate: delegate) { controller in - await controller.submitNewPassword(password, state: self) - } - } - - /// Submit user attributes (sign up). - public func submitAttributes(_ attributes: [String: Any], delegate: MSALNativeAuthFlowDelegate) { - run(delegate: delegate) { controller in - await controller.submitAttributes(attributes, state: self) - } - } - - /// Select an authentication method for MFA or strong-auth registration. - public func selectAuthMethod( - _ method: MSALAuthMethod, - verificationContact: String? = nil, - delegate: MSALNativeAuthFlowDelegate - ) { - run(delegate: delegate) { controller in - await controller.selectAuthMethod(method, verificationContact: verificationContact, state: self) - } - } - - /// Submit an MFA / strong-auth challenge response. - public func submitChallenge(_ challenge: String, delegate: MSALNativeAuthFlowDelegate) { - run(delegate: delegate) { controller in - await controller.submitChallenge(challenge, state: self) - } - } - - /// Request the server to resend the one-time code. - public func resendCode(delegate: MSALNativeAuthFlowDelegate) { - run(delegate: delegate) { controller in - await controller.resendCode(state: self) - } - } - - private func run( + /// Runs a controller operation for the given state and routes its response to the delegate. + /// The engine passes itself as the `state` the controller reads (`state.continuation`). + func run( delegate: MSALNativeAuthFlowDelegate, - operation: @escaping (MSALNativeAuthV2FlowControlling) async -> MSALNativeAuthV2FlowControllerResponse + operation: @escaping (MSALNativeAuthV2FlowControlling, MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse ) { Task { - let response = await operation(controller) + let response = await operation(controller, self) await dispatcher.dispatch(response, delegate: delegate) } } diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift new file mode 100644 index 0000000000..2a06d2b124 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server reports that some attributes were invalid and must be corrected. +/// Continue with ``submitAttributes(_:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthAttributesInvalidState: MSALNativeAuthState { + + /// The names of the attributes that were invalid. + public let attributeNames: [String] + + public init(attributeNames: [String]) { + self.attributeNames = attributeNames + super.init() + } + + /// Resubmit the corrected user attributes. + public func submitAttributes(_ attributes: [String: Any], delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitAttributes(attributes, state: state) + } + } + + public override var description: String { + return "attributesInvalid (\(attributeNames.joined(separator: ", ")))" + } +} + +/// Per-state delegate for the ``MSALNativeAuthAttributesInvalidState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthAttributesInvalidDelegate: MSALNativeAuthFlowDelegate { + + /// The server reports that some attributes were invalid and must be corrected. + /// Continue with ``MSALNativeAuthAttributesInvalidState/submitAttributes(_:delegate:)``. + /// - Parameters: + /// - state: The invalid-attributes state. + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onAttributesInvalid(state: MSALNativeAuthAttributesInvalidState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift new file mode 100644 index 0000000000..a4444b8f39 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server requires additional user attributes. +/// Continue with ``submitAttributes(_:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthAttributesRequiredState: MSALNativeAuthState { + + /// The attributes the server requires. + public let attributes: [MSALNativeAuthRequiredAttribute] + + public init(attributes: [MSALNativeAuthRequiredAttribute]) { + self.attributes = attributes + super.init() + } + + /// Submit user attributes. + public func submitAttributes(_ attributes: [String: Any], delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitAttributes(attributes, state: state) + } + } + + public override var description: String { + return "attributesRequired (\(attributes.map { $0.name }.joined(separator: ", ")))" + } +} + +/// Per-state delegate for the ``MSALNativeAuthAttributesRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthAttributesRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server requires additional user attributes. + /// Continue with ``MSALNativeAuthAttributesRequiredState/submitAttributes(_:delegate:)``. + /// - Parameters: + /// - state: The required-attributes state. + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onAttributesRequired(state: MSALNativeAuthAttributesRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift new file mode 100644 index 0000000000..b49997718c --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server requires the user to verify a one-time code. +/// Continue with ``submitCode(_:delegate:)`` or request a new code with ``resendCode(delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthCodeRequiredState: MSALNativeAuthState { + + /// A masked destination the code was sent to (e.g. a partially obfuscated email). + public let sentTo: String + + /// The channel the code was sent through. + public let channel: MSALNativeAuthChannelType + + /// The expected length of the code. + public let codeLength: Int + + public init(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { + self.sentTo = sentTo + self.channel = channel + self.codeLength = codeLength + super.init() + } + + /// Submit a one-time verification code. + public func submitCode(_ code: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitCode(code, state: state) + } + } + + /// Request the server to resend the one-time code. + public func resendCode(delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.resendCode(state: state) + } + } + + public override var description: String { + return "codeRequired (sentTo: \(sentTo), length: \(codeLength))" + } +} + +/// Per-state delegate for the ``MSALNativeAuthCodeRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthCodeRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server requires the user to verify a one-time code. + /// Continue with ``MSALNativeAuthCodeRequiredState/submitCode(_:delegate:)`` (or request a new + /// code with ``MSALNativeAuthCodeRequiredState/resendCode(delegate:)``). + /// - Parameters: + /// - state: The code-required state (destination, channel, expected length) that + /// also exposes the continuation methods. + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onCodeRequired(state: MSALNativeAuthCodeRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift new file mode 100644 index 0000000000..377351fb2b --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server requires multi-factor authentication; the user must select an auth method. +/// Continue with ``selectAuthMethod(_:verificationContact:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthMFARequiredState: MSALNativeAuthState { + + /// The authentication methods available for selection. + public let authMethods: [MSALAuthMethod] + + public init(authMethods: [MSALAuthMethod]) { + self.authMethods = authMethods + super.init() + } + + /// Select an authentication method for MFA. + public func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + delegate: MSALNativeAuthFlowDelegate + ) { + run(delegate: delegate) { controller, state in + await controller.selectAuthMethod(method, verificationContact: verificationContact, state: state) + } + } + + /// Select an authentication method for MFA, without an explicit verification contact. + public func selectAuthMethod(_ method: MSALAuthMethod, delegate: MSALNativeAuthFlowDelegate) { + selectAuthMethod(method, verificationContact: nil, delegate: delegate) + } + + public override var description: String { + return "mfaRequired" + } +} + +/// Per-state delegate for the ``MSALNativeAuthMFARequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthMFARequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server requires multi-factor authentication; the user must select an auth method. + /// Continue with ``MSALNativeAuthMFARequiredState/selectAuthMethod(_:verificationContact:delegate:)``. + /// - Parameters: + /// - state: The MFA-required state (available auth methods). + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onMFARequired(state: MSALNativeAuthMFARequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift new file mode 100644 index 0000000000..d202449016 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server sent an MFA challenge; the user must enter the verification code. +/// Continue with ``submitChallenge(_:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthMFAVerificationRequiredState: MSALNativeAuthState { + + /// A masked destination the code was sent to (e.g. a partially obfuscated email). + public let sentTo: String + + /// The channel the code was sent through. + public let channel: MSALNativeAuthChannelType + + /// The expected length of the code. + public let codeLength: Int + + public init(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { + self.sentTo = sentTo + self.channel = channel + self.codeLength = codeLength + super.init() + } + + /// Submit the MFA challenge response. + public func submitChallenge(_ challenge: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitChallenge(challenge, state: state) + } + } + + public override var description: String { + return "mfaVerificationRequired (sentTo: \(sentTo), length: \(codeLength))" + } +} + +/// Per-state delegate for the ``MSALNativeAuthMFAVerificationRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthMFAVerificationRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server sent an MFA challenge; the user must enter the verification code. + /// Continue with ``MSALNativeAuthMFAVerificationRequiredState/submitChallenge(_:delegate:)``. + /// - Parameters: + /// - state: The MFA verification state (destination, channel, expected length). + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onMFAVerificationRequired(state: MSALNativeAuthMFAVerificationRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthNewPasswordRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthNewPasswordRequiredState.swift new file mode 100644 index 0000000000..5497b7fd8a --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthNewPasswordRequiredState.swift @@ -0,0 +1,64 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server requires the user to enter a new password (self-service password reset). +/// Continue with ``submitNewPassword(_:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthNewPasswordRequiredState: MSALNativeAuthState { + + /// Submit a new password (self-service password reset). + public func submitNewPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitNewPassword(password, state: state) + } + } + + public override var description: String { + return "newPasswordRequired" + } +} + +/// Per-state delegate for the ``MSALNativeAuthNewPasswordRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthNewPasswordRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server requires the user to enter a new password (self-service password reset). + /// Continue with ``MSALNativeAuthNewPasswordRequiredState/submitNewPassword(_:delegate:)``. + /// - Parameters: + /// - state: The new-password-required state. + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onNewPasswordRequired(state: MSALNativeAuthNewPasswordRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthPasswordRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthPasswordRequiredState.swift new file mode 100644 index 0000000000..4fe515e56c --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthPasswordRequiredState.swift @@ -0,0 +1,64 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server requires the user to enter their password. +/// Continue with ``submitPassword(_:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthPasswordRequiredState: MSALNativeAuthState { + + /// Submit a password. + public func submitPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitPassword(password, state: state) + } + } + + public override var description: String { + return "passwordRequired" + } +} + +/// Per-state delegate for the ``MSALNativeAuthPasswordRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthPasswordRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server requires the user to enter their password. + /// Continue with ``MSALNativeAuthPasswordRequiredState/submitPassword(_:delegate:)``. + /// - Parameters: + /// - state: The password-required state. + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onPasswordRequired(state: MSALNativeAuthPasswordRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift new file mode 100644 index 0000000000..a8ad54a2d5 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift @@ -0,0 +1,73 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Base type for states the server can request during a Native Auth V2 (server-driven) flow. +/// +/// In V2 the server drives the flow: at each step the SDK reports a concrete +/// ``MSALNativeAuthState`` subclass through its dedicated ``MSALNativeAuthFlowDelegate`` callback +/// (e.g. ``MSALNativeAuthFlowDelegate/onCodeRequired(state:)``). The app then continues the flow by +/// calling the method(s) exposed on that concrete state — each state exposes only the +/// continuations valid for its step, so invalid calls are impossible. +/// +/// This is an abstract base class — the SDK always hands back one of its concrete subclasses to the +/// matching state-specific delegate callback, so apps never need to downcast the state. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthState: NSObject { + + /// The originating flow scenario for this state, set by the SDK when the state is created. + /// Reported alongside this state's delegate callbacks so the app can tell which flow produced + /// it. Internal detail — not part of the public API surface. + var scenario: MSALNativeAuthFlowScenario = .unknown + + /// The flow engine that continues the server-driven flow from this state, injected by the SDK + /// when the state is created. Internal detail — not part of the public API surface. `nil` only for + /// states an app constructs directly (which cannot advance a flow). + var engine: MSALNativeAuthFlowState? + + /// Forwards a continuation operation to the flow engine. If the state has no engine (e.g. it was + /// constructed directly by the app rather than handed back by the SDK), the delegate is notified + /// with a general error instead of silently doing nothing. + func run( + delegate: MSALNativeAuthFlowDelegate, + operation: @escaping (MSALNativeAuthV2FlowControlling, MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + ) { + guard let engine = engine else { + Task { @MainActor in + delegate.onFlowError( + error: MSALNativeAuthFlowError( + type: .generalError, + errorDescription: "This state cannot be used to continue the flow." + ), + scenario: self.scenario + ) + } + return + } + engine.run(delegate: delegate, operation: operation) + } +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift new file mode 100644 index 0000000000..81578bbdd5 --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server requires strong authentication registration (JIT); the user must select an auth method. +/// Continue with ``selectAuthMethod(_:verificationContact:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthStrongAuthRegistrationRequiredState: MSALNativeAuthState { + + /// The authentication methods available for registration. + public let authMethods: [MSALAuthMethod] + + public init(authMethods: [MSALAuthMethod]) { + self.authMethods = authMethods + super.init() + } + + /// Select an authentication method for strong-auth registration. + public func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + delegate: MSALNativeAuthFlowDelegate + ) { + run(delegate: delegate) { controller, state in + await controller.selectAuthMethod(method, verificationContact: verificationContact, state: state) + } + } + + /// Select an authentication method for strong-auth registration, without an explicit + /// verification contact. + public func selectAuthMethod(_ method: MSALAuthMethod, delegate: MSALNativeAuthFlowDelegate) { + selectAuthMethod(method, verificationContact: nil, delegate: delegate) + } + + public override var description: String { + return "strongAuthRegistrationRequired" + } +} + +/// Per-state delegate for the ``MSALNativeAuthStrongAuthRegistrationRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthStrongAuthRegistrationRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server requires strong authentication registration (JIT); the user must select an auth method. + /// Continue with ``MSALNativeAuthStrongAuthRegistrationRequiredState/selectAuthMethod(_:verificationContact:delegate:)``. + /// - Parameters: + /// - state: The strong-auth registration state (available auth methods). + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onStrongAuthRegistrationRequired(state: MSALNativeAuthStrongAuthRegistrationRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift new file mode 100644 index 0000000000..64905c20cf --- /dev/null +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The server sent a JIT challenge; the user must enter the verification code. +/// Continue with ``submitChallenge(_:delegate:)``. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objcMembers +public class MSALNativeAuthStrongAuthVerificationRequiredState: MSALNativeAuthState { + + /// A masked destination the code was sent to (e.g. a partially obfuscated email). + public let sentTo: String + + /// The channel the code was sent through. + public let channel: MSALNativeAuthChannelType + + /// The expected length of the code. + public let codeLength: Int + + public init(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { + self.sentTo = sentTo + self.channel = channel + self.codeLength = codeLength + super.init() + } + + /// Submit the strong-auth (JIT) challenge response. + public func submitChallenge(_ challenge: String, delegate: MSALNativeAuthFlowDelegate) { + run(delegate: delegate) { controller, state in + await controller.submitChallenge(challenge, state: state) + } + } + + public override var description: String { + return "strongAuthVerificationRequired (sentTo: \(sentTo), length: \(codeLength))" + } +} + +/// Per-state delegate for the ``MSALNativeAuthStrongAuthVerificationRequiredState`` step of a Native Auth V2 flow. +/// +/// Conform to this protocol (in addition to the terminal callbacks inherited from +/// ``MSALNativeAuthFlowDelegate``) to handle this state. Conforming is opt-in per state, but the +/// callback is required once you conform. +/// +/// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. +@objc +public protocol MSALNativeAuthStrongAuthVerificationRequiredDelegate: MSALNativeAuthFlowDelegate { + + /// The server sent a JIT challenge; the user must enter the verification code. + /// Continue with ``MSALNativeAuthStrongAuthVerificationRequiredState/submitChallenge(_:delegate:)``. + /// - Parameters: + /// - state: The strong-auth verification state (destination, channel, expected length). + /// - scenario: The flow that produced this callback. + /// - Note: If the app's delegate does not conform to this protocol, then + /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. + @MainActor func onStrongAuthVerificationRequired(state: MSALNativeAuthStrongAuthVerificationRequiredState, scenario: MSALNativeAuthFlowScenario) +} diff --git a/MSAL/src/public/MSALError.h b/MSAL/src/public/MSALError.h index 277b29ca5d..7b929a1e50 100644 --- a/MSAL/src/public/MSALError.h +++ b/MSAL/src/public/MSALError.h @@ -584,4 +584,9 @@ typedef NS_ENUM(NSInteger, MSALInternalError) Error is thrown when PSSO user registration attempted with no biometrics configured and sekey biometric policy is configured */ MSALErrorPSSOBiometricsNotEnrolled = -42744, + + /** + Error is thrown when ASWebAuthenticationSession is attempted to be used with invalid URL, which is not valid https scheme, or the URL is not in allowed domains. + */ + MSALInternalErrorInvalidASWebAuthenticationURL = -42745, }; diff --git a/MSAL/test/app/ios/MSALTestAppAcquireTokenViewController.m b/MSAL/test/app/ios/MSALTestAppAcquireTokenViewController.m index e8ab15b5d6..a20db9349b 100644 --- a/MSAL/test/app/ios/MSALTestAppAcquireTokenViewController.m +++ b/MSAL/test/app/ios/MSALTestAppAcquireTokenViewController.m @@ -695,7 +695,7 @@ - (void)onKeyboardWillShow:(NSNotification *)notification CGRect keyboardFrameEnd = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; keyboardFrameEnd = [self.view convertRect:keyboardFrameEnd fromView:nil]; - [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | curve animations:^{ + [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (curve << 16) animations:^{ self.acquireButtonsViewBottomConstraint.constant = keyboardFrameEnd.size.height - 49.0; // 49.0 is the height of a tab bar [self.view layoutIfNeeded]; } completion:nil]; @@ -707,7 +707,7 @@ - (void)onKeyboardWillHide:(NSNotification *)notification NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; - [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | curve animations:^{ + [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | (curve << 16) animations:^{ self.acquireButtonsViewBottomConstraint.constant = 0; [self.view layoutIfNeeded]; } completion:nil]; diff --git a/MSAL/test/automation/tests/MSALBaseUITest.m b/MSAL/test/automation/tests/MSALBaseUITest.m index dec83cf640..0abcf1f1d1 100644 --- a/MSAL/test/automation/tests/MSALBaseUITest.m +++ b/MSAL/test/automation/tests/MSALBaseUITest.m @@ -34,10 +34,12 @@ #import "MSIDTestAutomationApplication.h" #import "MSIDAutomationOperationResponseHandler.h" #import "MSIDKeyVaultAccountProvider.h" +#import "MSIDKeyVaultAppConfigProvider.h" #import "MSIDKeyVaultCredentialProvider.h" static MSIDTestConfigurationProvider *s_confProvider; static MSIDKeyVaultAccountProvider *s_keyVaultAccountProvider; +static MSIDKeyVaultAppConfigProvider *s_keyVaultAppConfigProvider; @implementation MSALBaseUITest @@ -50,6 +52,9 @@ + (void)setUp // Initialize Key Vault account provider if configured [self initializeKeyVaultAccountProviderWithConfigPath:confPath]; + + // Initialize Key Vault app config provider if configured + [self initializeKeyVaultAppConfigProviderWithConfigPath:confPath]; } + (void)initializeKeyVaultAccountProviderWithConfigPath:(NSString *)confPath @@ -125,6 +130,79 @@ + (void)initializeKeyVaultAccountProviderWithConfigPath:(NSString *)confPath self.class.keyVaultAccountProvider = s_keyVaultAccountProvider; } ++ (void)initializeKeyVaultAppConfigProviderWithConfigPath:(NSString *)confPath +{ + // Read config to get Key Vault app configs URL + NSData *configData = [NSData dataWithContentsOfFile:confPath]; + if (!configData) { + NSLog(@"[MSALBaseUITest] Could not read config file for Key Vault app config setup"); + return; + } + + NSError *jsonError = nil; + NSDictionary *config = [NSJSONSerialization JSONObjectWithData:configData options:0 error:&jsonError]; + if (!config) { + if (jsonError) { + NSLog(@"[MSALBaseUITest] Could not parse config JSON: %@", jsonError.localizedDescription); + } + else { + NSLog(@"[MSALBaseUITest] Could not parse config JSON"); + } + return; + } + + // Get Key Vault app configs URL + NSString *keyVaultAppConfigsURL = config[@"keyvault_app_configs_url"]; + + if (!keyVaultAppConfigsURL || keyVaultAppConfigsURL.length == 0) { + NSLog(@"[MSALBaseUITest] No keyvault_app_configs_url configured, using Lab API only"); + return; + } + + NSLog(@"[MSALBaseUITest] Initializing Key Vault app config provider with URL: %@", keyVaultAppConfigsURL); + + // Get certificate credentials from root config (used as fallback after Pipeline Cert and Azure CLI) + NSString *certData = config[@"certificate_data"]; + NSString *certPassword = config[@"certificate_password"]; + + // Create credential provider + // Credential chain: Pipeline Cert (env vars) → Azure CLI → Config Cert (fallback) + MSIDKeyVaultCredentialProvider *credentialProvider = [[MSIDKeyVaultCredentialProvider alloc] initWithCertificateContents:certData + certificatePassword:certPassword]; + + // Create app config provider + s_keyVaultAppConfigProvider = [[MSIDKeyVaultAppConfigProvider alloc] initWithKeyVaultURL:keyVaultAppConfigsURL + credentialProvider:credentialProvider]; + + // Fetch app configs synchronously during setup + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block NSError *fetchError = nil; + + [s_keyVaultAppConfigProvider fetchAppConfigsWithCompletionHandler:^(NSError * _Nullable error) { + fetchError = error; + dispatch_semaphore_signal(semaphore); + }]; + + // Wait up to 30 seconds for app configs to load + dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC); + if (dispatch_semaphore_wait(semaphore, timeout) != 0) { + NSLog(@"[MSALBaseUITest] Timeout loading Key Vault app configs, will use Lab API"); + s_keyVaultAppConfigProvider = nil; + return; + } + + if (fetchError) { + NSLog(@"[MSALBaseUITest] Failed to load Key Vault app configs: %@. Will use Lab API.", fetchError.localizedDescription); + s_keyVaultAppConfigProvider = nil; + return; + } + + NSLog(@"[MSALBaseUITest] Key Vault app configs loaded successfully"); + + // Set on the base class so MSIDBaseUITest can use it + self.class.keyVaultAppConfigProvider = s_keyVaultAppConfigProvider; +} + - (void)setUp { [super setUp]; @@ -256,6 +334,41 @@ - (void)acceptConsentIfNecessary:(XCUIElement *)elementToCheck // Check if title exists if (elementToCheck.exists) { + // The MSA "Verify your email" interstitial auto-focuses the email + // text field, raising the iOS keyboard. The keyboard covers the + // lower part of the page including the in-page consent button + // ("Use your password" / "Use your password instead"). The button + // matches via self.testApp.buttons[…] but synthesized taps on it + // land on the keyboard's hit area and get absorbed, so the page + // never progresses and the next test step times out. + // + // Dismiss the keyboard by tapping the "Verify your email" header + // — a static text in the webview, tapping it is a no-op for the + // page but defocuses the email field and dismisses the keyboard. + // This is more reliable than chasing the keyboard's "Done" + // accessory button, which lives under different parent element + // types across iOS versions and surface owners (SafariVC vs + // WKWebView). After the tap we poll briefly for the keyboard to + // actually collapse, otherwise the consent-button tap below can + // land on the still-collapsing keyboard's hit area. + // Note: iOS 26.3.1 seems to not detect the keyboard, still try + // to tap the header once. + XCUIElement *keyboard = self.testApp.keyboards.firstMatch; + if (keyboard.exists || i == 0) + { + XCUIElement *header = self.testApp.webViews.staticTexts[@"Verify your email"]; + if (header.exists) + { + [header msidTap]; + + NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:2.0]; + while (keyboard.exists && deadline.timeIntervalSinceNow > 0) + { + [NSThread sleepForTimeInterval:0.1]; + } + } + } + XCUIElement *button = self.testApp.buttons[consentButton]; // If consent button found, tap it and return if (button.exists) @@ -302,6 +415,11 @@ - (void)closeAuthUIUsingWebViewType:(MSIDWebviewType)webViewType { buttonTitle = @"Done"; } + + if (webViewType == MSIDWebviewTypeSafariViewController && osVersion > 26.0f && !usesPassedInWebView) + { + buttonTitle = @"Close"; + } XCUIElementQuery *elementQuery = [self.testApp.buttons matchingIdentifier:buttonTitle]; if(elementQuery.count > 1) diff --git a/MSAL/test/automation/tests/interactive/MSALChinaCloudUITests.m b/MSAL/test/automation/tests/interactive/MSALChinaCloudUITests.m index 0553b02e1a..35125b56ea 100644 --- a/MSAL/test/automation/tests/interactive/MSALChinaCloudUITests.m +++ b/MSAL/test/automation/tests/interactive/MSALChinaCloudUITests.m @@ -59,21 +59,25 @@ - (void)setUp - (void)testInstanceAwareWithNationalCloud_withChinaCloud { + XCTSkip(@"Disabled: Mooncake (mncmsidlab1.partner.onmschina.cn) China-cloud lab account is under ESTS smart lockout, so sign-in never completes. Re-enable once the China lab account is unlocked/reset and stable."); [self runInstanceAwareTestWithNationalCloud]; } - (void)testInstanceAwareWithNationalCloud_withOrganizationsAuthority_withChinaCloud { + XCTSkip(@"Disabled: Mooncake (mncmsidlab1.partner.onmschina.cn) China-cloud lab account is under ESTS smart lockout, so sign-in never completes. Re-enable once the China lab account is unlocked/reset and stable."); [self runInstanceAwareTestWithNationalCloud_withOrganizationsAuthority]; } - (void)testInstanceAwareWithNationalCloud_withOrganizationsAuthority_withLoginHintPresent_andEQP_withChinaCloud { + XCTSkip(@"Disabled: Mooncake (mncmsidlab1.partner.onmschina.cn) China-cloud lab account is under ESTS smart lockout, so sign-in never completes. Re-enable once the China lab account is unlocked/reset and stable."); [self runInstanceAwareTestWithNationalCloud_withOrganizationsAuthority_withLoginHintPresent_andEQP]; } - (void)testNonInstanceAwareWithNationalCloud_withSystemWebView_withChinaCloud { + XCTSkip(@"Disabled: Mooncake (mncmsidlab1.partner.onmschina.cn) China-cloud lab account is under ESTS smart lockout, so sign-in never completes. Re-enable once the China lab account is unlocked/reset and stable."); [self runNonInstanceAwareTestWithNationalCloud_withSystemWebView]; } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift index efe8c1b0e2..96b123530f 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift @@ -55,7 +55,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { private func makeState(links: [String: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowState { let continuation = MSALNativeAuthV2ContinuationState( - flowType: .resetPassword, + flowScenario: .passwordReset, continuationToken: continuationToken, links: links, username: "user@contoso.com", @@ -65,8 +65,8 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { return MSALNativeAuthFlowState(continuation: continuation, controller: sut) } - private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParameters { - let params = MSALNativeAuthResetPasswordParameters(username: "user@contoso.com") + private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParametersV2 { + let params = MSALNativeAuthResetPasswordParametersV2(username: "user@contoso.com") return params } @@ -97,7 +97,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_whenAuthorizationChallengeFails_returnsError() async { requestProviderMock.mockRequest() - validatorMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(kind: .generalError))] + validatorMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(type: .generalError))] let response = await sut.resetPassword(parameters: resetPasswordParameters()) @@ -113,7 +113,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") ] validatorMock.interactionResponses = [ - .error(MSALNativeAuthFlowError(kind: .userNotFound)) + .error(MSALNativeAuthFlowError(type: .userNotFound)) ] let response = await sut.resetPassword(parameters: resetPasswordParameters()) @@ -148,7 +148,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { func test_submitCode_whenInvalidCode_returnsErrorWithRetryState() async { requestProviderMock.mockRequest() validatorMock.interactionResponses = [ - .error(MSALNativeAuthFlowError(kind: .invalidCode)) + .error(MSALNativeAuthFlowError(type: .invalidCode)) ] let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) @@ -178,7 +178,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") - let state = makeState(flowType: .signIn, links: ["verify": URL(string: "https://contoso.com/verify")!]) + let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/verify")!]) let response = await sut.submitCode("12345678", state: state) @@ -195,7 +195,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") - let state = makeState(flowType: .signUp, links: ["verify": URL(string: "https://contoso.com/verify")!]) + let state = makeState(flowScenario: .signUp, links: ["verify": URL(string: "https://contoso.com/verify")!]) let response = await sut.submitCode("12345678", state: state) @@ -266,13 +266,13 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { // MARK: - signUp / signIn / MFA / JIT private func makeState( - flowType: MSALNativeAuthV2FlowType, + flowScenario: MSALNativeAuthFlowScenario, links: [String: URL], authMethods: [MSALAuthMethod] = [], continuationToken: String = "ct" ) -> MSALNativeAuthFlowState { let continuation = MSALNativeAuthV2ContinuationState( - flowType: flowType, + flowScenario: flowScenario, continuationToken: continuationToken, links: links, username: "user@contoso.com", @@ -292,7 +292,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { .codeRequired(continuationToken: "ct-2", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) ] - let response = await sut.signUp(parameters: MSALNativeAuthSignUpParameters(username: "user@contoso.com")) + let response = await sut.signUp(parameters: MSALNativeAuthSignUpParametersV2(username: "user@contoso.com")) guard case .actionRequired(let action, _) = response.result, case .codeRequired = action else { return XCTFail("Expected codeRequired action, got \(response.result)") @@ -316,7 +316,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { ] validatorMock.tokenResponse = .success(accessToken: "access-token") - let parameters = MSALNativeAuthSignUpParameters(username: "user@contoso.com") + let parameters = MSALNativeAuthSignUpParametersV2(username: "user@contoso.com") parameters.password = "Secret-Password-1" // Step 1: sign-up start. The server asks for `email`; the SDK auto-submits it and the app @@ -353,7 +353,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { .attributesRequired(continuationToken: "ct-email-2", attributes: [emailAttribute], submitHref: "https://contoso.com/submit") ] - let parameters = MSALNativeAuthSignUpParameters(username: "user@contoso.com") + let parameters = MSALNativeAuthSignUpParametersV2(username: "user@contoso.com") parameters.password = "Secret-Password-1" let response = await sut.signUp(parameters: parameters) @@ -422,7 +422,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [ .mfaRequired(continuationToken: "ct-mfa", methods: [method], challengeHref: "https://contoso.com/mfa/challenge") ] - let state = makeState(flowType: .signIn, links: ["verify": URL(string: "https://contoso.com/pw/verify")!]) + let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/pw/verify")!]) let response = await sut.submitPassword("password", state: state) @@ -437,7 +437,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") - let state = makeState(flowType: .signUp, links: ["submitAttributes": URL(string: "https://contoso.com/submitattributes")!]) + let state = makeState(flowScenario: .signUp, links: ["submitAttributes": URL(string: "https://contoso.com/submitattributes")!]) let response = await sut.submitAttributes(["displayName": "User"], state: state) @@ -452,7 +452,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") - let state = makeState(flowType: .signIn, links: ["verify": URL(string: "https://contoso.com/mfa/verify")!]) + let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/mfa/verify")!]) let response = await sut.submitChallenge("12345678", state: state) @@ -470,7 +470,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") - let state = makeState(flowType: .signIn, links: ["activate": URL(string: "https://contoso.com/jit/activate")!]) + let state = makeState(flowScenario: .signIn, links: ["activate": URL(string: "https://contoso.com/jit/activate")!]) let response = await sut.submitChallenge("12345678", state: state) diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift index 585e0e0278..528cc4b07c 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift @@ -41,7 +41,7 @@ class MSALNativeAuthV2FlowControllerMock: MSALNativeAuthV2FlowControlling { private func notImplementedResponse() -> MSALNativeAuthV2FlowControllerResponse { return MSALNativeAuthV2FlowControllerResponse( - .error(error: MSALNativeAuthFlowError(kind: .notImplemented), newState: nil), + .error(error: MSALNativeAuthFlowError(type: .notImplemented), newState: nil), correlationId: correlationId ) } diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift index 196d22fd23..7cb8ee08da 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift @@ -29,7 +29,7 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating var authorizeChallengeResponses: [MSALNativeAuthV2AuthorizeChallengeValidatedResponse] = [] var interactionResponses: [MSALNativeAuthV2InteractionValidatedResponse] = [] - var tokenResponse: MSALNativeAuthV2TokenValidatedResponse = .error(MSALNativeAuthFlowError(kind: .generalError)) + var tokenResponse: MSALNativeAuthV2TokenValidatedResponse = .error(MSALNativeAuthFlowError(type: .generalError)) private(set) var validateAuthorizeChallengeCallCount = 0 private(set) var validateInteractionCallCount = 0 @@ -37,13 +37,13 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating func validateAuthorizeChallenge( _ result: Result, - flowType: MSALNativeAuthV2FlowType + flowScenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { defer { validateAuthorizeChallengeCallCount += 1 } if validateAuthorizeChallengeCallCount < authorizeChallengeResponses.count { return authorizeChallengeResponses[validateAuthorizeChallengeCallCount] } - return .error(MSALNativeAuthFlowError(kind: .generalError)) + return .error(MSALNativeAuthFlowError(type: .generalError)) } func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse { @@ -51,7 +51,7 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating if validateInteractionCallCount < interactionResponses.count { return interactionResponses[validateInteractionCallCount] } - return .error(MSALNativeAuthFlowError(kind: .generalError)) + return .error(MSALNativeAuthFlowError(type: .generalError)) } func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse { diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 094753e29b..403b824579 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -77,34 +77,34 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateAuthorizeChallenge_withContinuationToken() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(.success(response), flowType: .resetPassword) + let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .resetPassword) XCTAssertEqual(result, .continuationToken(continuationToken: "ct", href: "https://contoso.com/reset")) } func test_validateAuthorizeChallenge_missingFlowLink_returnsError() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(.success(response), flowType: .signUp) + let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .signUp) XCTAssertEqual(result, .error(MSALNativeAuthFlowError( - kind: .generalError, + type: .generalError, errorDescription: "Invalid authorize-challenge response: missing 'sign_up' link" ))) } func test_validateAuthorizeChallenge_withAuthorizationCode() { let response = makeResponse(code: "auth-code") - let result = sut.validateAuthorizeChallenge(.success(response), flowType: .signIn) + let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .signIn) XCTAssertEqual(result, .authorizationCode(code: "auth-code")) } func test_validateAuthorizeChallenge_withServerError_returnsError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateAuthorizeChallenge(.success(response), flowType: .signIn) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .signIn) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateAuthorizeChallenge_withTransportFailure_returnsError() { - let result = sut.validateAuthorizeChallenge(.failure(ErrorMock.error), flowType: .signIn) + let result = sut.validateAuthorizeChallenge(.failure(ErrorMock.error), flowScenario: .signIn) guard case .error = result else { return XCTFail("Expected error") } @@ -160,25 +160,25 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateInteraction_updateAction_withoutUpdateLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct") let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_pollAction_withoutPollLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct") let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_verifyAction_withoutVerifyLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "verify", continuationToken: "ct", codeLength: 8, hint: "u***@contoso.com") let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_collectAttributesAction_withoutSubmitLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "collectAttributes", continuationToken: "ct") let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .generalError))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_continueState_returnsReadyToComplete() { @@ -191,21 +191,21 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "AADSTS50034 user not found", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .userNotFound))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .userNotFound))) } func test_validateInteraction_invalidGrant_mapsToInvalidCode() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "wrong code", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .invalidCode))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCode))) } func test_validateInteraction_invalidContinuationToken_mapsCorrectly() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad token", innerErrorCode: "invalidContinuationToken", correlationId: nil) let response = makeResponse(error: serverError) let result = sut.validateInteraction(.success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(kind: .invalidContinuationToken))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidContinuationToken))) } // MARK: - validateToken diff --git a/Package.swift b/Package.swift index 2e449e7e87..03f5d325e6 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,6 @@ let package = Package( targets: ["MSAL"]), ], targets: [ - .binaryTarget(name: "MSAL", url: "https://github.com/AzureAD/microsoft-authentication-library-for-objc/releases/download/2.11.0/MSAL.zip", checksum: "3631e345dfb7b2b923d8b9b4fc172d9bfe15bf1e50b716cdf875b7ea46179a5c") + .binaryTarget(name: "MSAL", url: "https://github.com/AzureAD/microsoft-authentication-library-for-objc/releases/download/2.13.0/MSAL.zip", checksum: "7823ca7150c7dedf71d3b51d3f167a78bde21e6deeb7551f371c17a46a6218a7") ] ) diff --git a/azure_pipelines/automation.yml b/azure_pipelines/automation.yml index bc6f649fd8..328ff73568 100644 --- a/azure_pipelines/automation.yml +++ b/azure_pipelines/automation.yml @@ -1,6 +1,10 @@ variables: - name: 'repositoryName' # Name of the repository value: 'AzureAD/microsoft-authentication-library-for-objc' + - name: GEM_HOME + value: $(Agent.TempDirectory)/.gem + - name: arch + value: arm64 schedules: # daily build will only be triggered if there is any change between the build and last successful build @@ -24,105 +28,153 @@ trigger: pr: none -pool: - vmImage: 'macOS-14' +resources: + repositories: + # Shared ACES macOS job template (pool + Xcode + tool setup) — source of truth. + # TODO: switch ref to 'main' once ameyapat/common-aces-config is merged. + - repository: pipelinesShared + type: git + name: IDDP/MSAL-ObjC-Pipelines + ref: refs/heads/main jobs: -- job: ui_automation - displayName: 'Run MSAL UI automation' - timeoutInMinutes: 360 - cancelTimeoutInMinutes: 5 - workspace: - clean: all +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: ui_automation + displayName: 'Run MSAL UI automation' + timeoutInMinutes: 360 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - template: templates/tests-with-conf-file.yml + parameters: + schema: 'MSAL Test Automation (iOS)' + destination: 'platform=iOS Simulator,name=$(IOS_SIM_DEVICE),OS=$(IOS_SIM_OS)' + sdk: 'iphonesimulator' + retry_tests: 'true' - steps: - - script: | - system_profiler SPHardwareDataType | grep "Processor Name" - displayName: 'Check System Information' - - - template: templates/tests-with-conf-file.yml - parameters: - schema: 'MSAL Test Automation (iOS)' - full_path: 'build/Build/Products/MSAL Test Automation (iOS)_iphonesimulator18.1-x86_64.xctestrun' - destination: 'platform=iOS Simulator,name=iPhone 16,OS=18.1' - sdk: 'iphonesimulator' - retry_tests: 'true' +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: e2e_test_native_auth_ios + displayName: 'Run MSAL E2E tests for iOS native auth' + timeoutInMinutes: 30 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - template: templates/tests-with-conf-file.yml + parameters: + schema: 'MSAL iOS Native Auth E2E Tests' + destination: 'platform=iOS Simulator,name=$(IOS_SIM_DEVICE),OS=$(IOS_SIM_OS)' + sdk: 'iphonesimulator' + retry_tests: 'false' -- job: e2e_test_native_auth_ios - displayName: 'Run MSAL E2E tests for iOS native auth' - timeoutInMinutes: 30 - cancelTimeoutInMinutes: 5 - workspace: - clean: all +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: e2e_test_native_auth_mac + displayName: 'Run MSAL E2E tests for macOS native auth' + timeoutInMinutes: 30 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - template: templates/tests-with-conf-file.yml + parameters: + schema: 'MSAL Mac Native Auth E2E Tests' + destination: 'platform=macOS' + sdk: 'macosx' + retry_tests: 'false' - steps: - - template: templates/tests-with-conf-file.yml - parameters: - schema: 'MSAL iOS Native Auth E2E Tests' - full_path: 'build/Build/Products/MSAL iOS Native Auth E2E Tests_MSAL iOS Native Auth E2E Tests_iphonesimulator18.1-x86_64.xctestrun' - destination: 'platform=iOS Simulator,name=iPhone 16,OS=18.1' - sdk: 'iphonesimulator' - retry_tests: 'false' +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: cocoapods_lib_lint_static + displayName: Run Cocoapods lib lint (static framework) + timeoutInMinutes: 30 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: true + installXcbeautify: false + steps: + - checkout: self + clean: true + submodules: true + fetchDepth: 1 + persistCredentials: true + path: s + - task: Bash@3 + displayName: Replace HEADER_SEARCH_PATHS for NativeAuth subspec in MSAL.podspec + inputs: + targetType: 'inline' + script: | + sed -i '' 's/"$SRCROOT\/MSAL"/__dir__/' MSAL.podspec + workingDirectory: '$(Build.SourcesDirectory)' + - task: Bash@3 + displayName: Run pod lib lint + inputs: + targetType: 'inline' + script: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + pod lib lint --fail-fast --allow-warnings --use-static-frameworks + workingDirectory: '$(Build.SourcesDirectory)' + - task: Bash@3 + displayName: Discard temporary changes made to MSAL.podspec + inputs: + targetType: 'inline' + script: | + git checkout -f -- MSAL.podspec + workingDirectory: '$(Build.SourcesDirectory)' -- job: e2e_test_native_auth_mac - displayName: 'Run MSAL E2E tests for macOS native auth' - timeoutInMinutes: 30 - cancelTimeoutInMinutes: 5 - workspace: - clean: all - - steps: - - template: templates/tests-with-conf-file.yml - parameters: - schema: 'MSAL Mac Native Auth E2E Tests' - full_path: 'build/Build/Products/MSAL Mac Native Auth E2E Tests_MSAL Mac Native Auth E2E Tests_macosx15.1-x86_64.xctestrun' - destination: 'platform=macOS' - sdk: 'macosx' - retry_tests: 'false' - -- job: cocoapods_lib_lint - displayName: Run Cocoapods lib lint - timeoutInMinutes: 30 - cancelTimeoutInMinutes: 5 - strategy: - maxParallel: 2 - matrix: - STATIC_LIBRARY: - args: "--fail-fast --allow-warnings --use-static-frameworks" - DYNAMIC_LIBRARY: - args: "--fail-fast --allow-warnings" - workspace: - clean: all - - steps: - - - checkout: self - clean: true - submodules: true - fetchDepth: 1 - persistCredentials: true - path: s - - - task: Bash@3 - displayName: Replace HEADER_SEARCH_PATHS for NativeAuth subspec in MSAL.podspec - inputs: - targetType: 'inline' - script: | - sed -i '' 's/"$SRCROOT\/MSAL"/__dir__/' MSAL.podspec - workingDirectory: '$(Build.SourcesDirectory)' - - - task: Bash@3 - displayName: Run pod lib lint - inputs: - targetType: 'inline' - script: | - pod lib lint $(args) - workingDirectory: '$(Build.SourcesDirectory)' - - - task: Bash@3 - displayName: Discard temporary changes made to MSAL.podspec - inputs: - targetType: 'inline' - script: | - git checkout -f -- MSAL.podspec - workingDirectory: '$(Build.SourcesDirectory)' +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: cocoapods_lib_lint_dynamic + displayName: Run Cocoapods lib lint (dynamic framework) + timeoutInMinutes: 30 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: true + installXcbeautify: false + steps: + - checkout: self + clean: true + submodules: true + fetchDepth: 1 + persistCredentials: true + path: s + - task: Bash@3 + displayName: Replace HEADER_SEARCH_PATHS for NativeAuth subspec in MSAL.podspec + inputs: + targetType: 'inline' + script: | + sed -i '' 's/"$SRCROOT\/MSAL"/__dir__/' MSAL.podspec + workingDirectory: '$(Build.SourcesDirectory)' + - task: Bash@3 + displayName: Run pod lib lint + inputs: + targetType: 'inline' + script: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + pod lib lint --fail-fast --allow-warnings + workingDirectory: '$(Build.SourcesDirectory)' + - task: Bash@3 + displayName: Discard temporary changes made to MSAL.podspec + inputs: + targetType: 'inline' + script: | + git checkout -f -- MSAL.podspec + workingDirectory: '$(Build.SourcesDirectory)' diff --git a/azure_pipelines/broker_submodule_check.yml b/azure_pipelines/broker_submodule_check.yml index a3ce4c72d7..982c5ffd0d 100644 --- a/azure_pipelines/broker_submodule_check.yml +++ b/azure_pipelines/broker_submodule_check.yml @@ -3,6 +3,10 @@ # Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/languages/xcode +variables: + - name: GEM_HOME + value: $(Agent.TempDirectory)/.gem + trigger: branches: include: @@ -15,9 +19,6 @@ pr: - '*' drafts: true -pool: - name: 'Azure Pipelines' - resources: repositories: - repository: azure-activedirectory-tokenbroker-for-objc @@ -30,165 +31,45 @@ resources: endpoint: 'MSAL ObjC Service Connection' name: AzureAD/WorkplaceJoin-for-iOS -jobs: -- job: 'Validate_Pull_Request' - strategy: - maxParallel: 2 - matrix: - IOS_LIB: - target: "ios_library" - MAC_LIB: - target: "mac_library" - # Note: visionOS validation moved to visionos-validation.yml (runs on PRs to main/release only) - displayName: Validate Pull Request - timeoutInMinutes: 75 - pool: - vmImage: 'macOS-15' - - steps: - - checkout: azure-activedirectory-tokenbroker-for-objc - displayName: 'Checkout Broker' - clean: false - submodules: false - fetchTags: true - persistCredentials: true - - - checkout: self - displayName: 'Checkout MSAL' - clean: false - submodules: false - fetchTags: true - path: 's/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc' - persistCredentials: true - - - task: Bash@3 - displayName: 'Checkout MSAL submodules + ADAL' - inputs: - workingDirectory: $(Pipeline.Workspace)/s - targetType: 'inline' - script: | - cd azure-activedirectory-tokenbroker-for-objc - git submodule update --init --recursive ADAuthenticationBroker/Frameworks/adal - cd ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc - git submodule update --init --recursive - - - checkout: WorkplaceJoin-for-iOS - displayName: 'Checkout WPJ' - clean: false - submodules: false - fetchTags: true - path: 's/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/WorkplaceJoin-for-iOS' - persistCredentials: true - - - task: AzureCLI@2 - inputs: - azureSubscription: 'AuthSdkResourceManager' - scriptType: 'pscore' - scriptLocation: 'inlineScript' - inlineScript: | - # if this fails, check out this bash script that includes diagnostics: - # https://gist.github.com/johnterickson/19f80a3e969e39f1000d118739176e62 - # uncomment these for more debugging spew - # GIT_TRACE=1 - # GIT_CURL_VERBOSE=1 - - # Note that the resoruce is specified to limit the token to Azure DevOps - $token = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv - Write-Host "##vso[task.setvariable variable=aadToken;issecret=true]$token" - - task: Bash@3 - displayName: 'Checkout NGC Submodules' - env: - AccessToken: $(MSAzureToken_encoded) - inputs: - workingDirectory: $(Pipeline.Workspace)/s - targetType: 'inline' - script: | - cd azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks - git -c http.https://msazure.visualstudio.com/DefaultCollection/One/_git/AD-MFA-NGCAuthentication.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init AD-MFA-NGCAuthentication - cd AD-MFA-NGCAuthentication - git -c http.https://msazure.visualstudio.com/DefaultCollection/One/_git/AD-MFA-NGCKeyProvider-ios.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init NGCKeyProvider - git -c http.https://msazure.visualstudio.com/DefaultCollection/One/_git/AD-MFA-MSAuthNetworking.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init MSAuthNetworking - - - task: Bash@3 - displayName: 'Checkout WPJ openssl-msft submodule' - inputs: - workingDirectory: $(Pipeline.Workspace)/s - targetType: 'inline' - script: | - cd azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/WorkplaceJoin-for-iOS - git -c http.https://msazure.visualstudio.com/DefaultCollection/PlatformCrypto/_git/openssl-msft.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init Frameworks/openssl-msft + # Shared ACES macOS job template (pool + Xcode + tool setup) — source of truth. + # TODO: switch ref to 'main' once ameyapat/common-aces-config is merged. + - repository: pipelinesShared + type: git + name: IDDP/MSAL-ObjC-Pipelines + ref: refs/heads/main - - task: Bash@3 - displayName: 'Update WPJ submodules' - inputs: - workingDirectory: $(Pipeline.Workspace)/s - targetType: 'inline' - script: | - cd azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/WorkplaceJoin-for-iOS - git submodule update --init --recursive Frameworks/microsoft-authentication-library-for-objc - - - task: Cache@2 - displayName: 'Cache CocoaPods' - condition: eq(variables['target'], 'mac_library') - inputs: - key: 'cocoapods | "$(Agent.OS)" | azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Podfile.lock' - path: '$(Pipeline.Workspace)/s/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Pods' - - - task: Bash@3 - displayName: 'Install CocoaPods dependencies' - condition: eq(variables['target'], 'mac_library') - env: - AAD_TOKEN: $(aadToken) - inputs: - workingDirectory: $(Pipeline.Workspace)/s/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker - targetType: 'inline' - script: | - GIT_CONFIG_COUNT=1 \ - GIT_CONFIG_KEY_0=http.https://office.visualstudio.com/.extraheader \ - GIT_CONFIG_VALUE_0="AUTHORIZATION: bearer $AAD_TOKEN" \ - pod install - retryCountOnTaskFailure: 1 - - - script: 'gem uninstall xcpretty -I --version 0.4.0' - displayName: 'Uninstall xcpretty v0.4.0' - - - script: 'gem install xcpretty -N -v 0.3.0' - displayName: 'Install xcpretty v0.3.0' - - - script: 'gem install slather -N' - displayName: 'Install slather' - - - task: UsePythonVersion@0 - displayName: 'Use Python 3.x' - - - task: Bash@3 - displayName: 'Select Xcode version' - inputs: - targetType: 'inline' - script: '/bin/bash -c "sudo xcode-select -s /Applications/Xcode_16.4.app"' - - - task: Bash@3 - displayName: 'Run a python script for Broker' - inputs: - targetType: 'inline' - script: | - cd azure-activedirectory-tokenbroker-for-objc - echo "executing build:./build.py" - { output=$(./build.py --show-build-settings --target $(target) 2>&1 1>&3-) ;} 3>&1 - final_status=$(<./build/status.txt) - echo "FINAL STATUS = ${final_status}" - echo "POSSIBLE ERRORS: ${output}" - - if [ $final_status != "0" ]; then - echo "Build & Testing Failed! \n ${output}" >&2 - fi - failOnStderr: true - - - task: Bash@3 - condition: always() - displayName: Cleanup - inputs: - targetType: 'inline' - script: | - rm -rf ./build/status.txt - +# Note: visionOS validation moved to visionos-validation.yml (runs on PRs to main/release only) +jobs: +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: Validate_Pull_Request_iOS + displayName: Validate Pull Request (iOS) + # 0 = use the maximum limit allowed by the pool. + timeoutInMinutes: 75 + selectXcode: false + installSlather: true + installXcpretty: true + installXcbeautify: true + installCocoaPods: true + installBundler: false + steps: + - template: /azure_pipelines/broker_build_steps.yml + parameters: + target: ios_library + +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: Validate_Pull_Request_MAC + displayName: Validate Pull Request (Mac) + # 0 = use the maximum limit allowed by the pool. + timeoutInMinutes: 75 + selectXcode: false + installSlather: true + installXcpretty: true + installXcbeautify: true + installCocoaPods: true + installBundler: false + steps: + - template: /azure_pipelines/broker_build_steps.yml + parameters: + target: mac_library diff --git a/azure_pipelines/pr-validation.yml b/azure_pipelines/pr-validation.yml index 38a645dc51..977fef3e3e 100644 --- a/azure_pipelines/pr-validation.yml +++ b/azure_pipelines/pr-validation.yml @@ -1,3 +1,9 @@ +variables: + - name: GEM_HOME + value: $(Agent.TempDirectory)/.gem + - name: arch + value: arm64 + # Pipeline will be triggered for PR & any updates on the PR on all branches pr: autoCancel: true @@ -15,230 +21,264 @@ trigger: exclude: - '*' +resources: + repositories: + # Shared ACES macOS job template (pool + Xcode + tool setup) — source of truth. + # TODO: switch ref to 'main' once ameyapat/common-aces-config is merged. + - repository: pipelinesShared + type: git + name: IDDP/MSAL-ObjC-Pipelines + ref: refs/heads/main + # Define parallel jobs that run build script for specified targets +# Note: visionOS validation moved to visionos-validation.yml (runs on PRs to main/release only) jobs: -- job: 'PRAssistant' - pool: - vmImage: 'ubuntu-latest' - timeOutInMinutes: 10 - displayName: Invoke pipeline PRAssistantInvoker to trigger copilot code review - steps: - - bash: | - echo "Source branch : $(System.PullRequest.SourceBranch) Target branch : $(System.PullRequest.targetBranchName)" - az pipelines run --project 'IDDP' --id 2728 --branch dev --organization 'https://dev.azure.com/IdentityDivision' --variables source_branch=$(System.PullRequest.SourceBranch) target_branch=$(System.PullRequest.targetBranchName) - displayName: 'Invoke pipeline PRAssistantInvoker' - env: - AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) - failOnStderr: false -- job: 'Validate_Pull_Request' - strategy: - maxParallel: 2 - matrix: - IOS_FRAMEWORK: - target: "iosFramework iosTestApp sampleIosApp sampleIosAppSwift" - MAC_FRAMEWORK: - target: "macFramework" - # Note: visionOS validation moved to visionos-validation.yml (runs on PRs to main/release only) - displayName: Validate Pull Request - pool: - vmImage: 'macOS-14' - timeOutInMinutes: 30 - - steps: - - script: | - /bin/bash -c "sudo xcode-select -s /Applications/Xcode_16.2.app" - displayName: 'Switch to use Xcode 16.2' - - task: CmdLine@2 - displayName: Uninstalling xcpretty v0.4.0 - inputs: - script: | - gem uninstall xcpretty -I --version 0.4.0 - failOnStderr: false - - task: CmdLine@2 - displayName: Installing xcpretty v0.3.0 - inputs: - script: | - gem install xcpretty -N -v 0.3.0 - failOnStderr: true - - task: CmdLine@2 - displayName: Installing dependencies - inputs: - script: | - gem install slather bundler -N - failOnStderr: true - - checkout: self - clean: true - submodules: true - fetchDepth: 1 - persistCredentials: false - - task: Bash@3 - displayName: Removing any lingering codecov files. These can cause issues when the xcode version changes - inputs: - targetType: 'inline' - script: | - find . -name "*.gcda" -print0 | xargs -0 rm - - task: ComponentGovernanceComponentDetection@0 - inputs: - alertWarningLevel: Low - - task: Bash@3 - displayName: Run Build script & check for Errors - inputs: - targetType: 'inline' - script: | - { output=$(./build.py --target $(target) 2>&1 1>&3-) ;} 3>&1 - final_status=$(<./build/status.txt) - echo "FINAL STATUS = ${final_status}" - echo "POSSIBLE ERRORS: ${output}" - - if [ $final_status != "0" ]; then - echo "Build & Testing Failed! \n ${output}" >&2 - fi - failOnStderr: true - - task: Bash@3 - condition: always() - displayName: Cleanup - inputs: - targetType: 'inline' - script: | - rm -rf ./build/status.txt - - task: PublishTestResults@2 - condition: always() - displayName: Publish Test Report - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '$(Agent.BuildDirectory)/s/build/reports/*' - failTaskOnFailedTests: true - testRunTitle: 'Test Run - $(target)' - -- job: fetch_pr_labels - displayName: 'Check for PR Label' - timeoutInMinutes: 5 - pool: - vmImage: 'macOS-14' - steps: - - script: | - url="https://api.github.com/repos/$BUILD_REPOSITORY_ID/issues/$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER/labels" - - echo "Fetching labels from $url " - - # Temporary file to store the raw response - temp_file=$(mktemp) - - # Store the response code and the raw response in separate variables - response_code=$(curl -s -w "%{http_code}" -o "$temp_file" "$url") - response_content=$(cat "$temp_file") - - echo "Response code: $response_code" - echo "Raw response: $response_content" - - if [[ "$response_code" -eq 200 ]]; then - label_names=$(echo $response_content | jq -r '.[].name' | paste -sd ', ' -) - echo "##vso[task.setvariable variable=PR_LABELS;isOutput=true]$label_names" - [ -z "$label_names" ] && echo "PR labels: " || echo "PR labels: $label_names" - else - echo "Request failed with status code: $response_code - Skipping Native Auth E2E tests as a preventive measure" - echo "##vso[task.setvariable variable=PR_LABELS;isOutput=true]'skip-native-auth-e2e-tests'" - fi - - name: fetchPrLabels - -- job: e2e_test_native_auth_ios - displayName: 'Run MSAL E2E tests for iOS native auth' - dependsOn: fetch_pr_labels - condition: and( succeeded(), not(contains(dependencies.fetch_pr_labels.outputs['fetchPrLabels.PR_LABELS'], 'skip-native-auth-e2e-tests')) ) - timeoutInMinutes: 30 - cancelTimeoutInMinutes: 5 - pool: - vmImage: 'macOS-14' - workspace: - clean: all - - steps: - - template: templates/tests-with-conf-file.yml - parameters: - schema: 'MSAL iOS Native Auth E2E Tests' - full_path: 'build/Build/Products/MSAL iOS Native Auth E2E Tests_MSAL iOS Native Auth E2E Tests_iphonesimulator18.1-x86_64.xctestrun' - destination: 'platform=iOS Simulator,name=iPhone 16,OS=18.1' - sdk: 'iphonesimulator' - retry_tests: false - -- job: e2e_test_native_auth_mac - displayName: 'Run MSAL E2E tests for macOS native auth' - dependsOn: fetch_pr_labels - condition: and( succeeded(), not(contains(dependencies.fetch_pr_labels.outputs['fetchPrLabels.PR_LABELS'], 'skip-native-auth-e2e-tests')) ) - timeoutInMinutes: 30 - cancelTimeoutInMinutes: 5 - pool: - vmImage: 'macOS-14' - workspace: - clean: all - - steps: - - template: templates/tests-with-conf-file.yml - parameters: - schema: 'MSAL Mac Native Auth E2E Tests' - full_path: 'build/Build/Products/MSAL Mac Native Auth E2E Tests_MSAL Mac Native Auth E2E Tests_macosx15.1-x86_64.xctestrun' - destination: 'platform=macOS' - sdk: 'macosx' - retry_tests: false - -- job: 'Validate_SPM_Integration' - displayName: Validate SPM Integration - pool: - vmImage: 'macOS-14' - timeOutInMinutes: 15 - workspace: - clean: all - - steps: - - - checkout: self - clean: true - submodules: true - fetchDepth: 1 - persistCredentials: true - path: s - - - script: | - /bin/bash -c "sudo xcode-select -s /Applications/Xcode_16.2.app" - displayName: 'Switch to use Xcode 16.2' - - # Note: visionOS SPM validation is in visionos-validation.yml (runs on PRs to main/release only) - - - task: Bash@3 - displayName: Set variable BRANCH_NAME to a temporary branch - inputs: - targetType: 'inline' - script: | - BRANCH_NAME_LOCAL="$(Build.SourceBranchName)-temp" - echo "##vso[task.setvariable variable=BRANCH_NAME]${BRANCH_NAME_LOCAL}" - - - task: Bash@3 - displayName: Checkout to temporary branch - inputs: - targetType: 'inline' - script: | - git checkout -b "${BRANCH_NAME}" - - - task: Bash@3 - displayName: Run SPM integration test script - inputs: - targetType: 'inline' - script: | - sh spm-integration-test.sh "${BRANCH_NAME}" - continueOnError: false - - - task: Bash@3 - condition: always() - displayName: Cleanup - inputs: - targetType: 'inline' - script: - cd ../.. - rm -rf "$SAMPLE_APP_TEMP_DIR" archive framework MSAL.zip - git checkout -- . - git fetch --quiet - git switch "$(Build.SourceBranchName)" - git branch -D "$BRANCH_NAME" - git push origin --delete "$BRANCH_NAME" +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: Validate_Pull_Request_iOS + displayName: Validate Pull Request (iOS) + timeoutInMinutes: 30 + selectXcode: false + installSlather: true + installXcpretty: true + installBundler: true + installCocoaPods: false + installXcbeautify: false + steps: + - checkout: self + clean: true + submodules: true + fetchDepth: 1 + persistCredentials: false + - task: Bash@3 + displayName: Removing any lingering codecov files. These can cause issues when the xcode version changes + inputs: + targetType: 'inline' + script: | + find . -name "*.gcda" -print0 | xargs -0 rm + - task: ComponentGovernanceComponentDetection@0 + inputs: + alertWarningLevel: Low + - task: Bash@3 + displayName: Run Build script & check for Errors + inputs: + targetType: 'inline' + script: | + { output=$(./build.py --target iosFramework iosTestApp sampleIosApp sampleIosAppSwift 2>&1 1>&3-) ;} 3>&1 + final_status=$(<./build/status.txt) + echo "FINAL STATUS = ${final_status}" + echo "POSSIBLE ERRORS: ${output}" + + if [ $final_status != "0" ]; then + echo "Build & Testing Failed! \n ${output}" >&2 + fi + failOnStderr: true + - task: Bash@3 + condition: always() + displayName: Cleanup + inputs: + targetType: 'inline' + script: | + rm -rf ./build/status.txt + - task: PublishTestResults@2 + condition: always() + displayName: Publish Test Report + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(Agent.BuildDirectory)/s/build/reports/*' + failTaskOnFailedTests: true + testRunTitle: 'Test Run - iosFramework' + +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: Validate_Pull_Request_macOS + displayName: Validate Pull Request (macOS) + timeoutInMinutes: 30 + selectXcode: false + installSlather: true + installXcpretty: true + installBundler: true + installCocoaPods: false + installXcbeautify: false + steps: + - checkout: self + clean: true + submodules: true + fetchDepth: 1 + persistCredentials: false + - task: Bash@3 + displayName: Removing any lingering codecov files. These can cause issues when the xcode version changes + inputs: + targetType: 'inline' + script: | + find . -name "*.gcda" -print0 | xargs -0 rm + - task: ComponentGovernanceComponentDetection@0 + inputs: + alertWarningLevel: Low + - task: Bash@3 + displayName: Run Build script & check for Errors + inputs: + targetType: 'inline' + script: | + { output=$(./build.py --target macFramework 2>&1 1>&3-) ;} 3>&1 + final_status=$(<./build/status.txt) + echo "FINAL STATUS = ${final_status}" + echo "POSSIBLE ERRORS: ${output}" + + if [ $final_status != "0" ]; then + echo "Build & Testing Failed! \n ${output}" >&2 + fi + failOnStderr: true + - task: Bash@3 + condition: always() + displayName: Cleanup + inputs: + targetType: 'inline' + script: | + rm -rf ./build/status.txt + - task: PublishTestResults@2 + condition: always() + displayName: Publish Test Report + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '$(Agent.BuildDirectory)/s/build/reports/*' + failTaskOnFailedTests: true + testRunTitle: 'Test Run - macFramework' + +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: fetch_pr_labels + displayName: 'Check for PR Label' + timeoutInMinutes: 5 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - script: | + url="https://api.github.com/repos/$BUILD_REPOSITORY_ID/issues/$SYSTEM_PULLREQUEST_PULLREQUESTNUMBER/labels" + + echo "Fetching labels from $url " + + # Temporary file to store the raw response + temp_file=$(mktemp) + + # Store the response code and the raw response in separate variables + response_code=$(curl -s -w "%{http_code}" -o "$temp_file" "$url") + response_content=$(cat "$temp_file") + + echo "Response code: $response_code" + echo "Raw response: $response_content" + + if [[ "$response_code" -eq 200 ]]; then + label_names=$(echo $response_content | jq -r '.[].name' | paste -sd ', ' -) + echo "##vso[task.setvariable variable=PR_LABELS;isOutput=true]$label_names" + [ -z "$label_names" ] && echo "PR labels: " || echo "PR labels: $label_names" + else + echo "Request failed with status code: $response_code - Skipping Native Auth E2E tests as a preventive measure" + echo "##vso[task.setvariable variable=PR_LABELS;isOutput=true]'skip-native-auth-e2e-tests'" + fi + name: fetchPrLabels + +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: e2e_test_native_auth_ios + displayName: 'Run MSAL E2E tests for iOS native auth' + timeoutInMinutes: 30 + dependsOn: [fetch_pr_labels] + condition: and( succeeded(), not(contains(dependencies.fetch_pr_labels.outputs['fetchPrLabels.PR_LABELS'], 'skip-native-auth-e2e-tests')) ) + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - template: templates/tests-with-conf-file.yml + parameters: + schema: 'MSAL iOS Native Auth E2E Tests' + destination: 'platform=iOS Simulator,name=$(IOS_SIM_DEVICE),OS=$(IOS_SIM_OS)' + sdk: 'iphonesimulator' + retry_tests: 'false' + +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: e2e_test_native_auth_mac + displayName: 'Run MSAL E2E tests for macOS native auth' + timeoutInMinutes: 30 + dependsOn: [fetch_pr_labels] + condition: and( succeeded(), not(contains(dependencies.fetch_pr_labels.outputs['fetchPrLabels.PR_LABELS'], 'skip-native-auth-e2e-tests')) ) + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - template: templates/tests-with-conf-file.yml + parameters: + schema: 'MSAL Mac Native Auth E2E Tests' + destination: 'platform=macOS' + sdk: 'macosx' + retry_tests: 'false' + +- template: Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared + parameters: + jobName: Validate_SPM_Integration + displayName: Validate SPM Integration + timeoutInMinutes: 15 + selectXcode: false + installSlather: false + installXcpretty: false + installBundler: false + installCocoaPods: false + installXcbeautify: false + steps: + - checkout: self + clean: true + submodules: true + fetchDepth: 1 + persistCredentials: true + path: s + + # Note: visionOS SPM validation is in visionos-validation.yml (runs on PRs to main/release only) + + - task: Bash@3 + displayName: Set variable BRANCH_NAME to a temporary branch + inputs: + targetType: 'inline' + script: | + BRANCH_NAME_LOCAL="${BUILD_SOURCEBRANCHNAME}-temp" + echo "##vso[task.setvariable variable=BRANCH_NAME]${BRANCH_NAME_LOCAL}" + + - task: Bash@3 + displayName: Checkout to temporary branch + inputs: + targetType: 'inline' + script: | + git checkout -b "${BRANCH_NAME}" + + - task: Bash@3 + displayName: Run SPM integration test script + inputs: + targetType: 'inline' + script: | + sh spm-integration-test.sh "${BRANCH_NAME}" + continueOnError: false + - task: Bash@3 + condition: always() + displayName: Cleanup + inputs: + targetType: 'inline' + workingDirectory: '$(Build.SourcesDirectory)' + script: | + cd "$(Build.SourcesDirectory)" + rm -rf "$SAMPLE_APP_TEMP_DIR" archive framework MSAL.zip + git checkout -- . + git fetch --quiet + git switch "$BUILD_SOURCEBRANCHNAME" + git branch -D "$BRANCH_NAME" + git push origin --delete "$BRANCH_NAME" diff --git a/azure_pipelines/templates/tests-with-conf-file.yml b/azure_pipelines/templates/tests-with-conf-file.yml index e46cd4972b..6c0d6a5287 100644 --- a/azure_pipelines/templates/tests-with-conf-file.yml +++ b/azure_pipelines/templates/tests-with-conf-file.yml @@ -1,3 +1,14 @@ +parameters: + - name: schema + type: string + - name: destination + type: string + - name: sdk + type: string + - name: retry_tests + type: string + default: 'false' + steps: - checkout: self clean: true @@ -6,12 +17,6 @@ steps: persistCredentials: true path: s - - task: Bash@3 - displayName: 'Select Xcode version' - inputs: - targetType: 'inline' - script: '/bin/bash -c "sudo xcode-select -s /Applications/Xcode_16.1.app"' - - task: Bash@3 displayName: Go to project folder inputs: @@ -19,11 +24,14 @@ steps: script: | cd $(Agent.BuildDirectory)/s - - script: 'gem uninstall xcpretty -I --version 0.4.0' + - script: gem uninstall xcpretty -I --version 0.4.0 || true displayName: 'Uninstall xcpretty v0.4.0' - - script: 'gem install xcpretty -v 0.3.0' - displayName: 'Install xcpretty v0.3.0' + - script: | + export PATH="$GEM_HOME/bin:$PATH" + gem install xcpretty -v 0.3.0 + gem install xcodeproj -N -v 1.27.0 + displayName: 'Install xcpretty v0.3.0 and xcodeproj' - task: AzureCLI@2 displayName: 'Read configuration from KeyVault' @@ -31,7 +39,7 @@ steps: azureSubscription: 'AuthSdkResourceManager' scriptType: 'bash' scriptLocation: 'inlineScript' - inlineScript: 'python get_automation_conf_file.py' + inlineScript: 'python3 get_automation_conf_file.py' workingDirectory: azure_pipelines/scripts/ - task: AzureKeyVault@2 @@ -46,6 +54,10 @@ steps: inputs: targetType: 'inline' script: | + set -o pipefail + export PATH="$GEM_HOME/bin:$PATH" + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 mv azure_pipelines/scripts/conf.json MSAL/test/automation/conf.json mkdir -p ~/aadtests cp MSAL/test/automation/conf.json ~/aadtests/conf.json @@ -68,8 +80,15 @@ steps: targetType: 'inline' script: | ls build/Build/Products/ + XCTESTRUN=$(ls "build/Build/Products/${{ parameters.schema }}"*.xctestrun 2>/dev/null | head -n1) + if [ -z "$XCTESTRUN" ]; then + echo "No .xctestrun file found for scheme '${{ parameters.schema }}'" + ls -la build/Build/Products/ || true + exit 1 + fi + echo "Using xctestrun: $XCTESTRUN" xcodebuild test-without-building \ - -xctestrun '${{ parameters.full_path }}' \ + -xctestrun "$XCTESTRUN" \ -destination '${{ parameters.destination }}' \ -retry-tests-on-failure \ -test-iterations 3 \ @@ -85,8 +104,15 @@ steps: targetType: 'inline' script: | ls build/Build/Products/ + XCTESTRUN=$(ls "build/Build/Products/${{ parameters.schema }}"*.xctestrun 2>/dev/null | head -n1) + if [ -z "$XCTESTRUN" ]; then + echo "No .xctestrun file found for scheme '${{ parameters.schema }}'" + ls -la build/Build/Products/ || true + exit 1 + fi + echo "Using xctestrun: $XCTESTRUN" xcodebuild test-without-building \ - -xctestrun '${{ parameters.full_path }}' \ + -xctestrun "$XCTESTRUN" \ -destination '${{ parameters.destination }}' \ -parallel-testing-enabled NO \ -resultBundlePath '$(Agent.BuildDirectory)/s/test_output/report.xcresult' @@ -100,11 +126,25 @@ steps: targetPath: '$(Agent.BuildDirectory)/s/test_output/' artifactName: 'TestOutputs Attempt - $(System.StageAttempt) - ${{ parameters.schema }}' publishLocation: 'pipeline' + + # Ensure the diagnostic-reports folder always exists so the publish step + # below doesn't fail when the test exited non-zero without producing a + # crash report (xctest timeout / assertion failure / xcodebuild error). + - task: Bash@3 + condition: failed() + displayName: "Ensure crash report folder exists" + inputs: + targetType: 'inline' + script: | + CRASH_DIR="$HOME/Library/Logs/DiagnosticReports" + mkdir -p "$CRASH_DIR" + echo "##vso[task.setvariable variable=crashReportFolder]$CRASH_DIR" + - task: PublishPipelineArtifact@1 condition: failed() displayName: "Publish crash report if any" inputs: - targetPath: "/Users/runner/Library/Logs/DiagnosticReports" + targetPath: "$(crashReportFolder)" artifactName: "crash folder" publishLocation: "pipeline" diff --git a/azure_pipelines/visionos-validation.yml b/azure_pipelines/visionos-validation.yml index 4362365f46..5adc2c36a0 100644 --- a/azure_pipelines/visionos-validation.yml +++ b/azure_pipelines/visionos-validation.yml @@ -149,8 +149,10 @@ jobs: inputs: targetType: 'inline' script: | - BRANCH_NAME_LOCAL="$(Build.SourceBranchName)-temp" + BRANCH_NAME_LOCAL="${SOURCE_BRANCH_NAME}-temp" echo "##vso[task.setvariable variable=BRANCH_NAME]${BRANCH_NAME_LOCAL}" + env: + SOURCE_BRANCH_NAME: $(Build.SourceBranchName) - task: Bash@3 displayName: Checkout to temporary branch @@ -177,9 +179,11 @@ jobs: rm -rf "$SAMPLE_APP_TEMP_DIR" archive framework MSAL.zip git checkout -- . git fetch --quiet - git switch "$(Build.SourceBranchName)" + git switch "$SOURCE_BRANCH_NAME" git branch -D "$BRANCH_NAME" git push origin --delete "$BRANCH_NAME" + env: + SOURCE_BRANCH_NAME: $(Build.SourceBranchName) - job: 'Validate_Broker_visionOS' displayName: Validate Broker visionOS Build diff --git a/build.py b/build.py index 73c910e802..72790e8685 100755 --- a/build.py +++ b/build.py @@ -29,19 +29,25 @@ import re import os import argparse +import platform import device_guids from timeit import default_timer as timer script_start_time = timer() -ios_sim_device_type = "iPhone 15" -ios_sim_device_exact_name = ios_sim_device_type + " Simulator \\(17.5\\)" -ios_sim_dest = "-destination 'platform=iOS Simulator,name=" + ios_sim_device_type + ",OS=17.5'" +# Simulator device/OS can be overridden via environment variables so the values +# can be centralized in the shared (common) pipeline configuration. Defaults are +# used when the environment variables are not set. +ios_sim_device_type = os.environ.get("IOS_SIM_DEVICE", "iPhone 17") +ios_sim_os = os.environ.get("IOS_SIM_OS", "26.1") +ios_sim_device_exact_name = ios_sim_device_type + " Simulator \\(" + ios_sim_os + "\\)" +ios_sim_dest = "-destination 'platform=iOS Simulator,name=" + ios_sim_device_type + ",OS=" + ios_sim_os + "'" ios_sim_flags = "-sdk iphonesimulator CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO" -vision_sim_device_exact_name = "Apple Vision Pro" -vision_sim_dest = "-destination 'platform=visionOS Simulator,name=" + vision_sim_device_exact_name + ",OS=1.2'" +vision_sim_device_exact_name = os.environ.get("VISION_SIM_DEVICE", "Apple Vision Pro") +vision_sim_os = os.environ.get("VISION_SIM_OS", "1.2") +vision_sim_dest = "-destination 'platform=visionOS Simulator,name=" + vision_sim_device_exact_name + ",OS=" + vision_sim_os + "'" vision_sim_flags = "-sdk xrsimulator CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO" default_workspace = "MSAL.xcworkspace" @@ -300,9 +306,26 @@ def do_codecov(self) : build_settings = self.get_build_settings(); build_dir = build_settings["BUILD_DIR"] derived_dir = os.path.normpath(build_dir + "/..") - device_guid = self.get_device_guid(); - - profile_data_path = derived_dir + "/ProfileData/" + device_guid + "/Coverage.profdata" + + # Xcode writes Coverage.profdata under ProfileData//. That + # UUID does not match the host hardware UUID on Apple Silicon, so prefer + # discovering the folder directly. Fall back to the legacy device-GUID path. + profile_data_dir = derived_dir + "/ProfileData" + profile_data_path = None + newest_mtime = None + if os.path.isdir(profile_data_dir) : + for entry in os.listdir(profile_data_dir) : + candidate = os.path.join(profile_data_dir, entry, "Coverage.profdata") + if os.path.isfile(candidate) : + mtime = os.path.getmtime(candidate) + if newest_mtime is None or mtime > newest_mtime : + newest_mtime = mtime + profile_data_path = candidate + + if profile_data_path is None : + device_guid = self.get_device_guid(); + profile_data_path = derived_dir + "/ProfileData/" + device_guid + "/Coverage.profdata" + if not os.path.isfile(profile_data_path) : print(ColorValues.FAIL + "Coverage data file missing! : " + profile_data_path + ColorValues.END) return -1 @@ -314,7 +337,10 @@ def do_codecov(self) : print(ColorValues.FAIL + "executable file missing! : " + executable_file_path + ColorValues.END) return -1 - command = "xcrun llvm-cov report -instr-profile " + profile_data_path + " -arch=\"x86_64\" -use-color " + executable_file_path + llvm_cov_arch = build_settings.get("CURRENT_ARCH") + if not llvm_cov_arch or llvm_cov_arch == "undefined_arch" : + llvm_cov_arch = platform.machine() or "x86_64" + command = "xcrun llvm-cov report -instr-profile " + profile_data_path + " -arch=\"" + llvm_cov_arch + "\" -use-color " + executable_file_path print(command) p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = True) diff --git a/spm-integration-test.sh b/spm-integration-test.sh index 3878752b54..98ca721e50 100644 --- a/spm-integration-test.sh +++ b/spm-integration-test.sh @@ -61,7 +61,12 @@ echo "Pushing MSAL.zip and Package.swift to $BRANCH_NAME" git add MSAL.zip Package.swift -git commit -m "Publish temporary Swift Package $current_date" +authorName=$(git log -1 --pretty=format:'%an') +authorEmail=$(git log -1 --pretty=format:'%ae') +git config user.email "${authorEmail}" +git config user.name "${authorName}" +author=$(git log -1 --pretty=format:'%an <%ae>') +git commit -m "Publish temporary Swift Package $current_date" -q --author="${author}" git push -f origin "$BRANCH_NAME" # Download and build Sample App (validates SPM package works end-to-end) @@ -86,5 +91,5 @@ else echo "Running the Sample App with the temporary Swift Package" xcodebuild -resolvePackageDependencies - xcodebuild -scheme NativeAuthSampleApp -configuration Release -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' clean build + xcodebuild -scheme NativeAuthSampleApp -configuration Release -sdk iphonesimulator -destination "platform=iOS Simulator,name=${IOS_SIM_DEVICE:-iPhone 17},OS=${IOS_SIM_OS:-26.1}" clean build fi \ No newline at end of file From 5fb022dab433de59fd9c52d68d16d583638d051d Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 12:08:30 +0100 Subject: [PATCH 27/85] Alignment for v1 and v2 interfaces for parameters and SOLID principles --- MSAL/MSAL.xcodeproj/project.pbxproj | 140 ++++++--- .../MSALNativeAuthControllerFactory.swift | 6 +- .../MSALNativeAuthFlowContinuationState.swift | 88 ++++++ ...ift => MSALNativeAuthFlowController.swift} | 50 ++-- ...MSALNativeAuthFlowControllerResponse.swift | 46 +++ ...ft => MSALNativeAuthFlowControlling.swift} | 26 +- .../v2/MSALNativeAuthFlowInternalState.swift} | 18 +- ...MSALNativeAuthFlowResponseDispatcher.swift | 144 ++++++++++ .../v2/MSALNativeAuthFlowResult.swift | 54 ++++ .../v2/MSALNativeAuthV2FlowResult.swift | 265 ------------------ .../MSALNativeAuthV2RequestConfigurator.swift | 141 ---------- ...AuthorizeChallengeContinueParameters.swift | 42 +++ ...hV2AuthorizeChallengeStartParameters.swift | 42 +++ .../MSALNativeAuthV2EntryParameters.swift | 45 +++ .../MSALNativeAuthV2HrefParameters.swift | 41 +++ .../MSALNativeAuthV2RequestTarget.swift | 40 +++ .../MSALNativeAuthV2Requestable.swift | 47 ++++ .../MSALNativeAuthV2TokenParameters.swift | 55 ++++ .../v2/state/MSALNativeAuthState.swift | 18 +- ...> MSALNativeAuthFlowControllerTests.swift} | 16 +- .../mock/MSALNativeAuthFactoriesMocks.swift | 10 +- ...=> MSALNativeAuthFlowControllerMock.swift} | 48 ++-- azure_pipelines/broker_build_steps.yml | 149 ++++++++++ 23 files changed, 993 insertions(+), 538 deletions(-) create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift rename MSAL/src/native_auth/controllers/v2/{MSALNativeAuthV2FlowController.swift => MSALNativeAuthFlowController.swift} (96%) create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControllerResponse.swift rename MSAL/src/native_auth/controllers/v2/{MSALNativeAuthV2FlowControlling.swift => MSALNativeAuthFlowControlling.swift} (70%) rename MSAL/src/native_auth/{public/state_machine/v2/MSALNativeAuthFlowState.swift => controllers/v2/MSALNativeAuthFlowInternalState.swift} (72%) create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift create mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift delete mode 100644 MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2Requestable.swift create mode 100644 MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift rename MSAL/test/unit/native_auth/controllers/v2/{MSALNativeAuthV2FlowControllerTests.swift => MSALNativeAuthFlowControllerTests.swift} (97%) rename MSAL/test/unit/native_auth/mock/v2/{MSALNativeAuthV2FlowControllerMock.swift => MSALNativeAuthFlowControllerMock.swift} (62%) create mode 100644 azure_pipelines/broker_build_steps.yml diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 0e9b2cb833..68bfbfc5cb 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; + 01F6FDA46510AF671264602E /* MSALNativeAuthFlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */; }; 01462653AC546A8B95A0D912 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; 06AAD69B63B7A013959ECEF7 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; 1FFBE815F16A5C07BCAEEA7A /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; @@ -421,8 +421,11 @@ 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; - 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */; }; - 33A0542A5B652892314FD6C8 /* MSALNativeAuthV2FlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */; }; + 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; + 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; + 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; + 0529EA054FE400F5647FCBA5 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; + D5449AE1C2AE8608DA837967 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; 38880DF423280C5900688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 38880DF523280C5A00688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; @@ -471,7 +474,7 @@ 7248CF9C2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 7248CF9D2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 7248CF9E2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; - 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */; }; + 76EEE63606562E71DCFDA606 /* MSALNativeAuthFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */; }; 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; @@ -489,8 +492,11 @@ 8D35C8F12A97BD2300BEC29A /* MSALNativeAuthRequiredAttributeOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D35C8F02A97BD2300BEC29A /* MSALNativeAuthRequiredAttributeOptions.swift */; }; 8D61F9A12A66AC9D00468E18 /* MSALNativeAuthRequestableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D61F9A02A66AC9D00468E18 /* MSALNativeAuthRequestableTests.swift */; }; 8DDF473F2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDF473E2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift */; }; - 8E0486CA55F25C1987E4067A /* MSALNativeAuthV2FlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */; }; - 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthV2FlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */; }; + 8E0486CA55F25C1987E4067A /* MSALNativeAuthFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */; }; + 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; + C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; + 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; + CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; 91AA24592BDF643A005037EA /* MSAL_Test_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA24582BDF643A005037EA /* MSAL_Test_App.swift */; }; 91AA245B2BDF643A005037EA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA245A2BDF643A005037EA /* ContentView.swift */; }; 91AA245D2BDF6440005037EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 91AA245C2BDF6440005037EA /* Assets.xcassets */; }; @@ -521,7 +527,7 @@ 963377C1211E14C600943EE0 /* MSALWebviewType.m in Sources */ = {isa = PBXBuildFile; fileRef = 963377BE211E14C600943EE0 /* MSALWebviewType.m */; }; 963377C2211E14C600943EE0 /* MSALWebviewType.m in Sources */ = {isa = PBXBuildFile; fileRef = 963377BE211E14C600943EE0 /* MSALWebviewType.m */; }; 963C89AB214BA1760051AFEE /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */; }; + 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; 9682A630218290FE00E37E63 /* MSALDefinitions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9682A62A218290F700E37E63 /* MSALDefinitions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96902DF320E1577500200E6F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DEC20E1574F00200E6F /* WebKit.framework */; }; 96902DF420E1578700200E6F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DEC20E1574F00200E6F /* WebKit.framework */; }; @@ -604,7 +610,7 @@ 9D02FCB728EF33FE003F791C /* MSALWPJMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = 9DA6473528EC2FF10014F44F /* MSALWPJMetaData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9D292B1028F05696007FE93C /* MSALWPJMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D292B0F28F05696007FE93C /* MSALWPJMetaData.m */; }; 9D292B1128F05696007FE93C /* MSALWPJMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = 9D292B0F28F05696007FE93C /* MSALWPJMetaData.m */; }; - 9D57981C31A9157AF52A29B9 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */; }; + 9D57981C31A9157AF52A29B9 /* MSALNativeAuthFlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */; }; A0274CBE24B432B100BD198D /* MSALAuthSchemeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */; }; A0274CBF24B432B100BD198D /* MSALAuthSchemeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */; }; A0274CD824B54A4E00BD198D /* MSALDevicePopManagerUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */; }; @@ -615,10 +621,10 @@ A0274CDE24B54C8900BD198D /* MSALDevicePopManagerUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */; }; A09AAFC324C00B3600C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A09AAFC424C00B3700C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A370E8BCE6A05E05ECC63027 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */; }; + A370E8BCE6A05E05ECC63027 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; - A939579E9B632F2EFA0447E6 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */; }; + A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; B203459521AF77FB00B221AA /* MSALRedirectUri.h in Headers */ = {isa = PBXBuildFile; fileRef = B203459221AF77FB00B221AA /* MSALRedirectUri.h */; settings = {ATTRIBUTES = (Public, ); }; }; B203459621AF77FB00B221AA /* MSALRedirectUri.m in Sources */ = {isa = PBXBuildFile; fileRef = B203459321AF77FB00B221AA /* MSALRedirectUri.m */; }; B203459721AF77FC00B221AA /* MSALRedirectUri.m in Sources */ = {isa = PBXBuildFile; fileRef = B203459321AF77FB00B221AA /* MSALRedirectUri.m */; }; @@ -1022,7 +1028,21 @@ CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; + 081C1B43CDAC5F4990EA68FB /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; + 3F2E65884A64B912E42B512D /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; + 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; + 02B7A67D74D6FCC9CD5BDAFA /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; + 76ACA3209E92AFC0CD4988B0 /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; + 7B9A32EA8EE3F6A20D0CFA80 /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; + 7662552749019C91197EA86B /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; + B5A1E2121EE36D3BC037113D /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; + AA5AB06A9DD86202FD19BFC8 /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; + 4CEDE2C62AFBCC69A07C8652 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; + A4B46554DA558BD5457129CF /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; + 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; + C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; + 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; D61A64941E5AA7D60086D120 /* MSALTestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64801E5AA7C60086D120 /* MSALTestAppDelegate.m */; }; D61A64951E5AA7D60086D120 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64811E5AA7C60086D120 /* main.m */; }; D61A64A91E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A649D1E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m */; }; @@ -1611,7 +1631,7 @@ DEFE87722CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; DEFE87732CA6BC91009D11DC /* CredentialsDelegateSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876E2CA6BC91009D11DC /* CredentialsDelegateSpies.swift */; }; DEFE87742CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; - E04298BA8ED8FBE431F561A2 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */; }; + E04298BA8ED8FBE431F561A2 /* MSALNativeAuthFlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */; }; E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */; }; E2025CC92B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025CC82B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift */; }; E2025D202B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025D1F2B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift */; }; @@ -1742,7 +1762,7 @@ E2F890052B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */; }; E2F8900E2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */; }; E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; - E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */; }; + E68C311BD4DDECABFAA212FD /* MSALNativeAuthFlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */; }; F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; @@ -2310,8 +2330,8 @@ 609AF9322256BD0C00E2978D /* MSALAccountsProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAccountsProviderTests.m; sourceTree = ""; }; 609AF958225B348900E2978D /* MSALTenantProfile+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTenantProfile+Internal.h"; sourceTree = ""; }; 60DEF15A1E67756800966664 /* MSAL Test App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = "MSAL Test App.entitlements"; path = "../../../../MSAL Test App.entitlements"; sourceTree = ""; }; - 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowState.swift; sourceTree = ""; }; - 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControllerMock.swift; sourceTree = ""; }; + 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowInternalState.swift; sourceTree = ""; }; + 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerMock.swift; sourceTree = ""; }; 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidator.swift; sourceTree = ""; }; 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALDeviceTokenResult+Internal.h"; sourceTree = ""; }; 7207E63E2FA97BBC008F6803 /* MSALDeviceTokenParametersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenParametersTests.m; sourceTree = ""; }; @@ -2320,8 +2340,11 @@ 7233F0882F885D05009C9602 /* MSALDeviceTokenParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenParameters.m; sourceTree = ""; }; 7248CF8E2F9AF2E90038E238 /* MSALDeviceTokenResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDeviceTokenResult.h; sourceTree = ""; }; 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenResult.m; sourceTree = ""; }; - 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControllerTests.swift; sourceTree = ""; }; - 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowResult.swift; sourceTree = ""; }; + 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerTests.swift; sourceTree = ""; }; + 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowResult.swift; sourceTree = ""; }; + B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowContinuationState.swift; sourceTree = ""; }; + 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerResponse.swift; sourceTree = ""; }; + 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowResponseDispatcher.swift; sourceTree = ""; }; 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCIAMAuthority.h; sourceTree = ""; }; 886F516329CCA58900F09471 /* MSALCIAMAuthority.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthority.m; sourceTree = ""; }; 88A25ED229E7185B00066311 /* MSALCIAMAuthorityTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthorityTests.m; sourceTree = ""; }; @@ -2580,12 +2603,19 @@ B2F4572F211C0B5C00818910 /* MSALBaseAADUITest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALBaseAADUITest.h; sourceTree = ""; }; B2F45744211E41C100818910 /* MSALB2CInteractiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALB2CInteractiveTests.m; sourceTree = ""; }; B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALWPJMetaData+Internal.h"; sourceTree = ""; }; - B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowControlling.swift; sourceTree = ""; }; + B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControlling.swift; sourceTree = ""; }; B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequestInterceptorBridge.swift; sourceTree = ""; }; C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; - C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2FlowController.swift; sourceTree = ""; }; + C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowController.swift; sourceTree = ""; }; C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorTests.swift; sourceTree = ""; }; D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestConfigurator.swift; sourceTree = ""; }; + 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Requestable.swift; sourceTree = ""; }; + CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestTarget.swift; sourceTree = ""; }; + 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2AuthorizeChallengeStartParameters.swift; sourceTree = ""; }; + D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift; sourceTree = ""; }; + 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2TokenParameters.swift; sourceTree = ""; }; + 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2EntryParameters.swift; sourceTree = ""; }; + EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefParameters.swift; sourceTree = ""; }; D61A63F11E5979200086D120 /* MSALResult+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALResult+Internal.h"; sourceTree = ""; }; D61A64331E5A29580086D120 /* MSAL Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MSAL Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D61A64661E5AA6B40086D120 /* msal__test_app__ios.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = msal__test_app__ios.xcconfig; sourceTree = ""; }; @@ -3170,6 +3200,7 @@ F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */, B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */, D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */, + 5731594DCD4698A57CAB2D10 /* parameters */, ); name = v2; path = v2; @@ -3178,9 +3209,13 @@ 1718011BA0E3C2154663D13F /* v2 */ = { isa = PBXGroup; children = ( - 7E10D43EA340CAA107F73114 /* MSALNativeAuthV2FlowResult.swift */, - B414350D2B1EE1FA349DC550 /* MSALNativeAuthV2FlowControlling.swift */, - C3B8230A5B6672389A1A6075 /* MSALNativeAuthV2FlowController.swift */, + 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */, + B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */, + 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */, + 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */, + 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */, + B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */, + C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */, ); name = v2; path = v2; @@ -3628,7 +3663,6 @@ 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, 58F36BCB7532CC3D9D60EE3A /* state */, AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */, - 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowState.swift */, ); path = v2; sourceTree = ""; @@ -3677,7 +3711,7 @@ 9279009AB4669F3B2A4D66FD /* v2 */ = { isa = PBXGroup; children = ( - 657374069BB444E4D7FF440C /* MSALNativeAuthV2FlowControllerMock.swift */, + 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */, 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */, 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */, ); @@ -5516,12 +5550,26 @@ E6E09369971B71B5529C8CFE /* v2 */ = { isa = PBXGroup; children = ( - 76FDC0929F7E8268E1076A6F /* MSALNativeAuthV2FlowControllerTests.swift */, + 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */, ); name = v2; path = v2; sourceTree = ""; }; + 5731594DCD4698A57CAB2D10 /* parameters */ = { + isa = PBXGroup; + children = ( + 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */, + CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */, + 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */, + D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */, + 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */, + 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */, + EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */, + ); + path = parameters; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -7406,10 +7454,13 @@ 285F58542C5BA33B00F4EFA4 /* MSALNativeAuthSignInIntrospectRequestParameters.swift in Sources */, 28DE70D629FAC16700EB75AA /* MSALNativeAuthSignInResponseValidator.swift in Sources */, C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */, - 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowState.swift in Sources */, - 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthV2FlowResult.swift in Sources */, - 01F6FDA46510AF671264602E /* MSALNativeAuthV2FlowControlling.swift in Sources */, - 8E0486CA55F25C1987E4067A /* MSALNativeAuthV2FlowController.swift in Sources */, + 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowInternalState.swift in Sources */, + 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthFlowResult.swift in Sources */, + C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */, + 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */, + CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */, + 01F6FDA46510AF671264602E /* MSALNativeAuthFlowControlling.swift in Sources */, + 8E0486CA55F25C1987E4067A /* MSALNativeAuthFlowController.swift in Sources */, CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */, FADE0000000000000000AA02 /* HALResource.swift in Sources */, 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, @@ -7420,6 +7471,13 @@ 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, + 081C1B43CDAC5F4990EA68FB /* MSALNativeAuthV2Requestable.swift in Sources */, + 3F2E65884A64B912E42B512D /* MSALNativeAuthV2RequestTarget.swift in Sources */, + 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */, + 02B7A67D74D6FCC9CD5BDAFA /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */, + 76ACA3209E92AFC0CD4988B0 /* MSALNativeAuthV2TokenParameters.swift in Sources */, + 7B9A32EA8EE3F6A20D0CFA80 /* MSALNativeAuthV2EntryParameters.swift in Sources */, + 7662552749019C91197EA86B /* MSALNativeAuthV2HrefParameters.swift in Sources */, 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */, E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */, @@ -7727,10 +7785,13 @@ DE8DC4972C6621A600534E8F /* SignInAfterResetPasswordDelegate.swift in Sources */, DE8DC4512C66218900534E8F /* MSALNativeAuthInternalError.swift in Sources */, E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */, - 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowState.swift in Sources */, - 33A0542A5B652892314FD6C8 /* MSALNativeAuthV2FlowResult.swift in Sources */, - E68C311BD4DDECABFAA212FD /* MSALNativeAuthV2FlowControlling.swift in Sources */, - 76EEE63606562E71DCFDA606 /* MSALNativeAuthV2FlowController.swift in Sources */, + 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */, + 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */, + 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */, + 0529EA054FE400F5647FCBA5 /* MSALNativeAuthFlowControllerResponse.swift in Sources */, + D5449AE1C2AE8608DA837967 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */, + E68C311BD4DDECABFAA212FD /* MSALNativeAuthFlowControlling.swift in Sources */, + 76EEE63606562E71DCFDA606 /* MSALNativeAuthFlowController.swift in Sources */, 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */, FADE0000000000000000AA03 /* HALResource.swift in Sources */, 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, @@ -7741,6 +7802,13 @@ 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, + B5A1E2121EE36D3BC037113D /* MSALNativeAuthV2Requestable.swift in Sources */, + AA5AB06A9DD86202FD19BFC8 /* MSALNativeAuthV2RequestTarget.swift in Sources */, + 4CEDE2C62AFBCC69A07C8652 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */, + A4B46554DA558BD5457129CF /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */, + 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */, + C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */, + 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */, F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */, D661864E47CDC268A2FA5EEF /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */, @@ -7934,10 +8002,10 @@ 9BD2765B2A0E7E7D00FBD033 /* ResetPasswordCodeSentStateTests.swift in Sources */, E2F626B32A781CE300C4A303 /* SignInDelegatesSpies.swift in Sources */, E22427EA2B065EAE0006C55E /* SignUpVerifyCodeDelegateDispatcherTests.swift in Sources */, - A370E8BCE6A05E05ECC63027 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */, + A370E8BCE6A05E05ECC63027 /* MSALNativeAuthFlowControllerMock.swift in Sources */, 5E471E84AA33CFA840BBA964 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, - E04298BA8ED8FBE431F561A2 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */, + E04298BA8ED8FBE431F561A2 /* MSALNativeAuthFlowControllerTests.swift in Sources */, A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -8117,10 +8185,10 @@ 28FB1BB62E0AF1F90065B784 /* MSALNativeAuthPublicClientApplicationConfigObjCTest.m in Sources */, 289E44BA2C9D843F00F6B9D7 /* MFARequestChallengeErrorTests.swift in Sources */, DE8DC56C2C66221C00534E8F /* MSALNativeLoggingTests.swift in Sources */, - A939579E9B632F2EFA0447E6 /* MSALNativeAuthV2FlowControllerMock.swift in Sources */, + A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */, 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, - 9D57981C31A9157AF52A29B9 /* MSALNativeAuthV2FlowControllerTests.swift in Sources */, + 9D57981C31A9157AF52A29B9 /* MSALNativeAuthFlowControllerTests.swift in Sources */, A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift index 44cd8e1bc1..0c7775c126 100644 --- a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift +++ b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift @@ -28,7 +28,7 @@ protocol MSALNativeAuthControllerBuildable { func makeJITController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthJITControlling func makeResetPasswordController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthResetPasswordControlling func makeCredentialsController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthCredentialsControlling - func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthV2FlowControlling + func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling } final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { @@ -58,7 +58,7 @@ final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { return MSALNativeAuthCredentialsController(config: config, cacheAccessor: cacheAccessor) } - func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthV2FlowControlling { - return MSALNativeAuthV2FlowController(config: config, cacheAccessor: cacheAccessor) + func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling { + return MSALNativeAuthFlowController(config: config, cacheAccessor: cacheAccessor) } } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift new file mode 100644 index 0000000000..6e92db73a7 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -0,0 +1,88 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Internal continuation context carried by a ``MSALNativeAuthFlowInternalState``. +/// +/// Holds the opaque server `continuation_token` and the resolved `_links` hrefs +/// the SDK must follow to advance the server-driven flow. +struct MSALNativeAuthFlowContinuationState { + let flowScenario: MSALNativeAuthFlowScenario + let continuationToken: String + /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", + /// "challenge", "enroll", "activate", "submitAttributes"). Per-method links are keyed "method:". + let links: [String: URL] + let username: String? + let sentToHint: String? + let codeLength: Int? + /// Auth methods offered for MFA / strong-auth (JIT) selection. + let authMethods: [MSALAuthMethod] + /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final + /// `/token` exchange. Threaded through every step. + let scopes: [String] + /// Values supplied by the app at sign-up start (keyed by attribute id, e.g. "email"/"password") + /// that the SDK submits automatically when the server issues a `collectAttributes` request for + /// them. Deliberately kept internal so the app never sees them again; must never be logged or + /// exposed on the public surface. + let signUpAutofillValues: [String: Any]? + /// Attribute ids already auto-submitted from ``signUpAutofillValues`` during this sign-up flow. + /// Used to detect when the server re-requests an attribute we already sent (e.g. after a + /// validation failure) so the SDK surfaces an error to the app instead of resending in a loop. + /// Carries no attribute values. + let signUpAutofillSubmittedIds: Set + + init( + flowScenario: MSALNativeAuthFlowScenario, + continuationToken: String, + links: [String: URL], + username: String?, + sentToHint: String? = nil, + codeLength: Int? = nil, + authMethods: [MSALAuthMethod] = [], + scopes: [String] = [], + signUpAutofillValues: [String: Any]? = nil, + signUpAutofillSubmittedIds: Set = [] + ) { + self.flowScenario = flowScenario + self.continuationToken = continuationToken + self.links = links + self.username = username + self.sentToHint = sentToHint + self.codeLength = codeLength + self.authMethods = authMethods + self.scopes = scopes + self.signUpAutofillValues = signUpAutofillValues + self.signUpAutofillSubmittedIds = signUpAutofillSubmittedIds + } + + func link(_ relation: String) -> URL? { + return links[relation] + } + + /// The challenge / enroll link associated with a specific auth method. + func methodLink(for methodId: String) -> URL? { + return links["method:\(methodId)"] + } +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift similarity index 96% rename from MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift rename to MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 88f6563aea..4e5ac0b478 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -28,7 +28,7 @@ import Foundation // swiftlint:disable file_length // swiftlint:disable:next type_body_length -final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNativeAuthV2FlowControlling, MSALNativeAuthTokenRequestHandling { +final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNativeAuthFlowControlling, MSALNativeAuthTokenRequestHandling { private let config: MSALNativeAuthInternalConfiguration private let requestProvider: MSALNativeAuthV2RequestProviding @@ -68,7 +68,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Entry points - func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthV2FlowControllerResponse { + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse { let flowScenario: MSALNativeAuthFlowScenario = .signUp let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpStart, context: context) @@ -111,7 +111,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } // swiftlint:disable:next function_body_length - func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse { let flowScenario: MSALNativeAuthFlowScenario = .signIn let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil @@ -210,7 +210,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthV2FlowControllerResponse { + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse { let flowScenario: MSALNativeAuthFlowScenario = .passwordReset let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordStart, context: context) @@ -253,7 +253,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Continuation // swiftlint:disable:next function_body_length - func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let continuation = state.continuation @@ -318,7 +318,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } - func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignInSubmitPassword, context: context) let continuation = state.continuation @@ -351,7 +351,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } // swiftlint:disable:next function_body_length - func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmit, context: context) let continuation = state.continuation @@ -427,7 +427,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpSubmitAttributes, context: context) let continuation = state.continuation @@ -465,8 +465,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa func selectAuthMethod( _ method: MSALAuthMethod, verificationContact: String?, - state: MSALNativeAuthFlowState - ) async -> MSALNativeAuthV2FlowControllerResponse { + state: MSALNativeAuthFlowInternalState + ) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let continuation = state.continuation @@ -550,7 +550,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa } } - func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFASubmitChallenge, context: context) let continuation = state.continuation @@ -601,7 +601,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa ) } - func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordResendCode, context: context) let continuation = state.continuation @@ -683,7 +683,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext - ) -> MSALNativeAuthV2FlowControllerResponse { + ) -> MSALNativeAuthFlowControllerResponse { switch result { case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): let newState = makeState( @@ -720,11 +720,11 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, - recoverableState: MSALNativeAuthFlowState? = nil, + recoverableState: MSALNativeAuthFlowInternalState? = nil, fallbackHint: String? = nil, signUpAutofillValues: [String: Any]? = nil, signUpAutofillSubmittedIds: Set = [] - ) async -> MSALNativeAuthV2FlowControllerResponse { + ) async -> MSALNativeAuthFlowControllerResponse { switch result { case .readyToComplete(let token): return await completeWithToken( @@ -897,7 +897,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: [String], event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext - ) async -> MSALNativeAuthV2FlowControllerResponse { + ) async -> MSALNativeAuthFlowControllerResponse { let codeResult = await performAuthorizeChallengeContinue(flowScenario: flowScenario, continuationToken: continuationToken, context: context) guard case .authorizationCode(let code) = codeResult else { return failure(codeResult, event: event, context: context, scenario: flowScenario) @@ -998,7 +998,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa scopes: [String] = [], signUpAutofillValues: [String: Any]? = nil, signUpAutofillSubmittedIds: Set = [] - ) -> MSALNativeAuthFlowState { + ) -> MSALNativeAuthFlowInternalState { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) var resolvedLinks: [String: URL] = [:] for (relation, href) in links { @@ -1011,7 +1011,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa resolvedLinks["method:\(methodId)"] = url } } - let continuation = MSALNativeAuthV2ContinuationState( + let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, continuationToken: continuationToken, links: resolvedLinks, @@ -1023,7 +1023,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa signUpAutofillValues: signUpAutofillValues, signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) - return MSALNativeAuthFlowState(continuation: continuation, controller: self) + return MSALNativeAuthFlowInternalState(continuation: continuation, controller: self) } /// Converts embedded HAL methods into public ``MSALAuthMethod`` objects plus a map of each @@ -1086,11 +1086,11 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa // MARK: - Response construction private func response( - _ result: MSALNativeAuthV2FlowResult, + _ result: MSALNativeAuthFlowResult, context: MSALNativeAuthRequestContext, scenario: MSALNativeAuthFlowScenario - ) -> MSALNativeAuthV2FlowControllerResponse { - return MSALNativeAuthV2FlowControllerResponse( + ) -> MSALNativeAuthFlowControllerResponse { + return MSALNativeAuthFlowControllerResponse( result, correlationId: context.correlationId(), scenario: scenario @@ -1102,7 +1102,7 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, scenario: MSALNativeAuthFlowScenario - ) -> MSALNativeAuthV2FlowControllerResponse { + ) -> MSALNativeAuthFlowControllerResponse { let error: MSALNativeAuthFlowError if case .error(let flowError) = validated { error = flowError @@ -1118,8 +1118,8 @@ final class MSALNativeAuthV2FlowController: MSALNativeAuthBaseController, MSALNa event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, scenario: MSALNativeAuthFlowScenario, - newState: MSALNativeAuthFlowState? - ) -> MSALNativeAuthV2FlowControllerResponse { + newState: MSALNativeAuthFlowInternalState? + ) -> MSALNativeAuthFlowControllerResponse { let error: MSALNativeAuthFlowError if case .error(let flowError) = validated { error = flowError diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControllerResponse.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControllerResponse.swift new file mode 100644 index 0000000000..35319ba3f0 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControllerResponse.swift @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Wraps the controller result with the correlation id and an optional telemetry update closure. +struct MSALNativeAuthFlowControllerResponse { + let result: MSALNativeAuthFlowResult + let correlationId: UUID + /// The public scenario reported to the app (defaults to `.unknown` when the flow is undetermined). + let scenario: MSALNativeAuthFlowScenario + let telemetryUpdate: ((Result) -> Void)? + + init( + _ result: MSALNativeAuthFlowResult, + correlationId: UUID, + scenario: MSALNativeAuthFlowScenario = .unknown, + telemetryUpdate: ((Result) -> Void)? = nil + ) { + self.result = result + self.correlationId = correlationId + self.scenario = scenario + self.telemetryUpdate = telemetryUpdate + } +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControlling.swift similarity index 70% rename from MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift rename to MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControlling.swift index 4fdab7c516..8672d35379 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowControlling.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControlling.swift @@ -28,34 +28,34 @@ import Foundation /// /// A single unified controller backs every V2 flow. Each method performs one step /// (or, for entry methods, the initial sequence of steps that the server can complete -/// without app interaction) and returns a ``MSALNativeAuthV2FlowControllerResponse``. -protocol MSALNativeAuthV2FlowControlling { +/// without app interaction) and returns a ``MSALNativeAuthFlowControllerResponse``. +protocol MSALNativeAuthFlowControlling { // MARK: - Entry points - func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthV2FlowControllerResponse + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse - func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthV2FlowControllerResponse + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse - func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse // MARK: - Continuation - func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse - func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse - func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse - func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse func selectAuthMethod( _ method: MSALAuthMethod, verificationContact: String?, - state: MSALNativeAuthFlowState - ) async -> MSALNativeAuthV2FlowControllerResponse + state: MSALNativeAuthFlowInternalState + ) async -> MSALNativeAuthFlowControllerResponse - func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse - func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse } diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift similarity index 72% rename from MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift rename to MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift index 5dbafca5cf..246d4481cb 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift @@ -24,31 +24,31 @@ import Foundation -/// Internal engine that continues a Native Auth V2 (server-driven) flow. +/// Internal state that continues a Native Auth V2 (server-driven) flow. /// -/// The SDK creates one engine per flow and hands it to each concrete ``MSALNativeAuthState`` it +/// The SDK creates one internal state per flow and hands it to each concrete ``MSALNativeAuthState`` it /// produces (via the dispatcher). The concrete state's public continuation methods (e.g. /// `submitCode(_:delegate:)`) forward to ``run(delegate:operation:)``, which invokes the matching /// controller operation and routes the resulting response back through the dispatcher. /// /// This type carries no public API surface — apps interact only with the concrete /// ``MSALNativeAuthState`` subclasses. -class MSALNativeAuthFlowState { +class MSALNativeAuthFlowInternalState { - let continuation: MSALNativeAuthV2ContinuationState - private let controller: MSALNativeAuthV2FlowControlling + let continuation: MSALNativeAuthFlowContinuationState + private let controller: MSALNativeAuthFlowControlling private let dispatcher = MSALNativeAuthFlowResponseDispatcher() - init(continuation: MSALNativeAuthV2ContinuationState, controller: MSALNativeAuthV2FlowControlling) { + init(continuation: MSALNativeAuthFlowContinuationState, controller: MSALNativeAuthFlowControlling) { self.continuation = continuation self.controller = controller } - /// Runs a controller operation for the given state and routes its response to the delegate. - /// The engine passes itself as the `state` the controller reads (`state.continuation`). + /// Runs a controller operation and routes its response to the delegate. + /// Passes itself as the value the controller reads (`internalState.continuation`). func run( delegate: MSALNativeAuthFlowDelegate, - operation: @escaping (MSALNativeAuthV2FlowControlling, MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + operation: @escaping (MSALNativeAuthFlowControlling, MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse ) { Task { let response = await operation(controller, self) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift new file mode 100644 index 0000000000..963182f38d --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift @@ -0,0 +1,144 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Routes a controller response to the appropriate ``MSALNativeAuthFlowDelegate`` callback. +/// +/// V2 uses opt-in, per-state delegate protocols that extend ``MSALNativeAuthFlowDelegate``. For an +/// `actionRequired` result the dispatcher builds the concrete ``MSALNativeAuthState`` for the step, +/// wires it to the internal state, and — if the app's delegate conforms to that step's protocol — +/// invokes its dedicated callback. If the app does not conform, the terminal +/// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. +struct MSALNativeAuthFlowResponseDispatcher { + + func dispatch(_ response: MSALNativeAuthFlowControllerResponse, delegate: MSALNativeAuthFlowDelegate) async { + let scenario = response.scenario + switch response.result { + case .actionRequired(let action, let internalState): + await dispatchAction(action, internalState: internalState, scenario: scenario, response: response, delegate: delegate) + case .completed(let result): + await delegate.onFlowCompleted(result: result, scenario: scenario) + response.telemetryUpdate?(.success(())) + case .error(let error, _): + await delegate.onFlowError(error: error, scenario: scenario) + case .browserRequired(let url, _): + let error = MSALNativeAuthFlowError( + type: .browserRequired, + errorDescription: "The flow must continue in a web browser: \(url.absoluteString)", + correlationId: response.correlationId + ) + await delegate.onFlowError(error: error, scenario: scenario) + response.telemetryUpdate?(.success(())) + } + } + + private func dispatchAction( + _ action: MSALNativeAuthAction, + internalState: MSALNativeAuthFlowInternalState, + scenario: MSALNativeAuthFlowScenario, + response: MSALNativeAuthFlowControllerResponse, + delegate: MSALNativeAuthFlowDelegate + ) async { + switch action { + case .codeRequired(let sentTo, let channel, let codeLength): + let state = MSALNativeAuthCodeRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthCodeRequiredDelegate.self) { await $0.onCodeRequired(state: state, scenario: scenario) } + case .passwordRequired: + let state = MSALNativeAuthPasswordRequiredState() + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthPasswordRequiredDelegate.self) { await $0.onPasswordRequired(state: state, scenario: scenario) } + case .newPasswordRequired: + let state = MSALNativeAuthNewPasswordRequiredState() + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthNewPasswordRequiredDelegate.self) { await $0.onNewPasswordRequired(state: state, scenario: scenario) } + case .attributesRequired(let attributes): + let state = MSALNativeAuthAttributesRequiredState(attributes: attributes) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthAttributesRequiredDelegate.self) { + await $0.onAttributesRequired(state: state, scenario: scenario) + } + case .attributesInvalid(let attributeNames): + let state = MSALNativeAuthAttributesInvalidState(attributeNames: attributeNames) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthAttributesInvalidDelegate.self) { await $0.onAttributesInvalid(state: state, scenario: scenario) } + case .mfaRequired(let authMethods): + let state = MSALNativeAuthMFARequiredState(authMethods: authMethods) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthMFARequiredDelegate.self) { await $0.onMFARequired(state: state, scenario: scenario) } + case .mfaVerificationRequired(let sentTo, let channel, let codeLength): + let state = MSALNativeAuthMFAVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthMFAVerificationRequiredDelegate.self) { + await $0.onMFAVerificationRequired(state: state, scenario: scenario) + } + case .strongAuthRegistrationRequired(let authMethods): + let state = MSALNativeAuthStrongAuthRegistrationRequiredState(authMethods: authMethods) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthStrongAuthRegistrationRequiredDelegate.self) { + await $0.onStrongAuthRegistrationRequired(state: state, scenario: scenario) + } + case .strongAuthVerificationRequired(let sentTo, let channel, let codeLength): + let state = MSALNativeAuthStrongAuthVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) + await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, + as: MSALNativeAuthStrongAuthVerificationRequiredDelegate.self) { + await $0.onStrongAuthVerificationRequired(state: state, scenario: scenario) + } + } + } + + /// Wires a concrete state to its internal state and originating scenario, then invokes the app's + /// per-state callback when the delegate conforms to `Delegate`; otherwise reports `notImplemented` + /// through the terminal error callback. + private func deliver( + _ state: MSALNativeAuthState, + internalState: MSALNativeAuthFlowInternalState, + scenario: MSALNativeAuthFlowScenario, + response: MSALNativeAuthFlowControllerResponse, + delegate: MSALNativeAuthFlowDelegate, + as delegateType: Delegate.Type, + callback: (Delegate) async -> Void + ) async { + state.internalState = internalState + state.scenario = scenario + if let typedDelegate = delegate as? Delegate { + await callback(typedDelegate) + response.telemetryUpdate?(.success(())) + } else { + await notImplemented(delegate: delegate, scenario: scenario, correlationId: response.correlationId) + } + } + + private func notImplemented( + delegate: MSALNativeAuthFlowDelegate, + scenario: MSALNativeAuthFlowScenario, + correlationId: UUID + ) async { + await delegate.onFlowError( + error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: correlationId), + scenario: scenario + ) + } +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift new file mode 100644 index 0000000000..1a3f2bba55 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Identifies which V2 flow a ``MSALNativeAuthFlowInternalState`` belongs to. +extension MSALNativeAuthFlowScenario { + + /// The server-driven flows the SDK follows when resolving `authorize-challenge` links. + static let authorizeChallengeFlows: [MSALNativeAuthFlowScenario] = [.signUp, .signIn, .passwordReset, .unknown] + + /// The `authorize-challenge` link relation this flow follows. + var link: String { + switch self { + case .signUp: + return "sign_up" + case .signIn: + return "sign_in" + case .passwordReset: + return "reset_password" + case .unknown: + return "unknown" + } + } +} + +/// Result produced by the unified V2 controller for a single step of a flow. +enum MSALNativeAuthFlowResult { + case actionRequired(action: MSALNativeAuthAction, newState: MSALNativeAuthFlowInternalState) + case completed(MSALNativeAuthUserAccountResult) + case error(error: MSALNativeAuthFlowError, newState: MSALNativeAuthFlowInternalState?) + case browserRequired(url: URL, newState: MSALNativeAuthFlowInternalState) +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift deleted file mode 100644 index e819e38b76..0000000000 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthV2FlowResult.swift +++ /dev/null @@ -1,265 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Identifies which V2 flow a ``MSALNativeAuthFlowState`` belongs to. -extension MSALNativeAuthFlowScenario { - - /// The server-driven flows the SDK follows when resolving `authorize-challenge` links. - static let authorizeChallengeFlows: [MSALNativeAuthFlowScenario] = [.signUp, .signIn, .passwordReset, .unknown] - - /// The `authorize-challenge` link relation this flow follows. - var link: String { - switch self { - case .signUp: - return "sign_up" - case .signIn: - return "sign_in" - case .passwordReset: - return "reset_password" - case .unknown: - return "unknown" - } - } -} - -/// Internal continuation context carried by a ``MSALNativeAuthFlowState``. -/// -/// Holds the opaque server `continuation_token` and the resolved `_links` hrefs -/// the SDK must follow to advance the server-driven flow. -struct MSALNativeAuthV2ContinuationState { - let flowScenario: MSALNativeAuthFlowScenario - let continuationToken: String - /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", - /// "challenge", "enroll", "activate", "submitAttributes"). Per-method links are keyed "method:". - let links: [String: URL] - let username: String? - let sentToHint: String? - let codeLength: Int? - /// Auth methods offered for MFA / strong-auth (JIT) selection. - let authMethods: [MSALAuthMethod] - /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final - /// `/token` exchange. Threaded through every step. - let scopes: [String] - /// Values supplied by the app at sign-up start (keyed by attribute id, e.g. "email"/"password") - /// that the SDK submits automatically when the server issues a `collectAttributes` request for - /// them. Deliberately kept internal so the app never sees them again; must never be logged or - /// exposed on the public surface. - let signUpAutofillValues: [String: Any]? - /// Attribute ids already auto-submitted from ``signUpAutofillValues`` during this sign-up flow. - /// Used to detect when the server re-requests an attribute we already sent (e.g. after a - /// validation failure) so the SDK surfaces an error to the app instead of resending in a loop. - /// Carries no attribute values. - let signUpAutofillSubmittedIds: Set - - init( - flowScenario: MSALNativeAuthFlowScenario, - continuationToken: String, - links: [String: URL], - username: String?, - sentToHint: String? = nil, - codeLength: Int? = nil, - authMethods: [MSALAuthMethod] = [], - scopes: [String] = [], - signUpAutofillValues: [String: Any]? = nil, - signUpAutofillSubmittedIds: Set = [] - ) { - self.flowScenario = flowScenario - self.continuationToken = continuationToken - self.links = links - self.username = username - self.sentToHint = sentToHint - self.codeLength = codeLength - self.authMethods = authMethods - self.scopes = scopes - self.signUpAutofillValues = signUpAutofillValues - self.signUpAutofillSubmittedIds = signUpAutofillSubmittedIds - } - - func link(_ relation: String) -> URL? { - return links[relation] - } - - /// The challenge / enroll link associated with a specific auth method. - func methodLink(for methodId: String) -> URL? { - return links["method:\(methodId)"] - } -} - -/// Result produced by the unified V2 controller for a single step of a flow. -enum MSALNativeAuthV2FlowResult { - case actionRequired(action: MSALNativeAuthAction, newState: MSALNativeAuthFlowState) - case completed(MSALNativeAuthUserAccountResult) - case error(error: MSALNativeAuthFlowError, newState: MSALNativeAuthFlowState?) - case browserRequired(url: URL, newState: MSALNativeAuthFlowState) -} - -/// Wraps the controller result with the correlation id and an optional telemetry update closure. -struct MSALNativeAuthV2FlowControllerResponse { - let result: MSALNativeAuthV2FlowResult - let correlationId: UUID - /// The public scenario reported to the app (defaults to `.unknown` when the flow is undetermined). - let scenario: MSALNativeAuthFlowScenario - let telemetryUpdate: ((Result) -> Void)? - - init( - _ result: MSALNativeAuthV2FlowResult, - correlationId: UUID, - scenario: MSALNativeAuthFlowScenario = .unknown, - telemetryUpdate: ((Result) -> Void)? = nil - ) { - self.result = result - self.correlationId = correlationId - self.scenario = scenario - self.telemetryUpdate = telemetryUpdate - } -} - -/// Routes a controller response to the appropriate ``MSALNativeAuthFlowDelegate`` callback. -/// -/// V2 uses opt-in, per-state delegate protocols that extend ``MSALNativeAuthFlowDelegate``. For an -/// `actionRequired` result the dispatcher builds the concrete ``MSALNativeAuthState`` for the step, -/// wires it to the flow engine, and — if the app's delegate conforms to that step's protocol — -/// invokes its dedicated callback. If the app does not conform, the terminal -/// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. -struct MSALNativeAuthFlowResponseDispatcher { - - @MainActor - func dispatch(_ response: MSALNativeAuthV2FlowControllerResponse, delegate: MSALNativeAuthFlowDelegate) async { - let scenario = response.scenario - switch response.result { - case .actionRequired(let action, let engine): - await dispatchAction(action, engine: engine, scenario: scenario, response: response, delegate: delegate) - case .completed(let result): - await delegate.onFlowCompleted(result: result, scenario: scenario) - response.telemetryUpdate?(.success(())) - case .error(let error, _): - await delegate.onFlowError(error: error, scenario: scenario) - case .browserRequired(let url, _): - let error = MSALNativeAuthFlowError( - type: .browserRequired, - errorDescription: "The flow must continue in a web browser: \(url.absoluteString)", - correlationId: response.correlationId - ) - await delegate.onFlowError(error: error, scenario: scenario) - response.telemetryUpdate?(.success(())) - } - } - - @MainActor - private func dispatchAction( - _ action: MSALNativeAuthAction, - engine: MSALNativeAuthFlowState, - scenario: MSALNativeAuthFlowScenario, - response: MSALNativeAuthV2FlowControllerResponse, - delegate: MSALNativeAuthFlowDelegate - ) async { - switch action { - case .codeRequired(let sentTo, let channel, let codeLength): - let state = MSALNativeAuthCodeRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthCodeRequiredDelegate.self) { await $0.onCodeRequired(state: state, scenario: scenario) } - case .passwordRequired: - let state = MSALNativeAuthPasswordRequiredState() - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthPasswordRequiredDelegate.self) { await $0.onPasswordRequired(state: state, scenario: scenario) } - case .newPasswordRequired: - let state = MSALNativeAuthNewPasswordRequiredState() - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthNewPasswordRequiredDelegate.self) { await $0.onNewPasswordRequired(state: state, scenario: scenario) } - case .attributesRequired(let attributes): - let state = MSALNativeAuthAttributesRequiredState(attributes: attributes) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthAttributesRequiredDelegate.self) { - await $0.onAttributesRequired(state: state, scenario: scenario) - } - case .attributesInvalid(let attributeNames): - let state = MSALNativeAuthAttributesInvalidState(attributeNames: attributeNames) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthAttributesInvalidDelegate.self) { await $0.onAttributesInvalid(state: state, scenario: scenario) } - case .mfaRequired(let authMethods): - let state = MSALNativeAuthMFARequiredState(authMethods: authMethods) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthMFARequiredDelegate.self) { await $0.onMFARequired(state: state, scenario: scenario) } - case .mfaVerificationRequired(let sentTo, let channel, let codeLength): - let state = MSALNativeAuthMFAVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthMFAVerificationRequiredDelegate.self) { - await $0.onMFAVerificationRequired(state: state, scenario: scenario) - } - case .strongAuthRegistrationRequired(let authMethods): - let state = MSALNativeAuthStrongAuthRegistrationRequiredState(authMethods: authMethods) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthStrongAuthRegistrationRequiredDelegate.self) { - await $0.onStrongAuthRegistrationRequired(state: state, scenario: scenario) - } - case .strongAuthVerificationRequired(let sentTo, let channel, let codeLength): - let state = MSALNativeAuthStrongAuthVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) - await deliver(state, engine: engine, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthStrongAuthVerificationRequiredDelegate.self) { - await $0.onStrongAuthVerificationRequired(state: state, scenario: scenario) - } - } - } - - /// Wires a concrete state to the engine and invokes the app's per-state callback when the delegate - /// conforms to `Delegate`; otherwise reports `notImplemented` through the terminal error callback. - @MainActor - private func deliver( - _ state: MSALNativeAuthState, - engine: MSALNativeAuthFlowState, - scenario: MSALNativeAuthFlowScenario, - response: MSALNativeAuthV2FlowControllerResponse, - delegate: MSALNativeAuthFlowDelegate, - as delegateType: Delegate.Type, - callback: (Delegate) async -> Void - ) async { - prepare(state, engine: engine, scenario: scenario) - if let typedDelegate = delegate as? Delegate { - await callback(typedDelegate) - response.telemetryUpdate?(.success(())) - } else { - await notImplemented(delegate: delegate, scenario: scenario, correlationId: response.correlationId) - } - } - - /// Wires a freshly built concrete state to the flow engine and stamps the originating scenario. - private func prepare(_ state: MSALNativeAuthState, engine: MSALNativeAuthFlowState, scenario: MSALNativeAuthFlowScenario) { - state.scenario = scenario - state.engine = engine - } - - @MainActor - private func notImplemented( - delegate: MSALNativeAuthFlowDelegate, - scenario: MSALNativeAuthFlowScenario, - correlationId: UUID - ) async { - await delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: correlationId), - scenario: scenario - ) - } -} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift index 6ee7c1c1a6..8dcebd7b31 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -26,147 +26,6 @@ import Foundation @_implementationOnly import MSAL_Private -// MARK: - Request parameters - -/// Describes a single V2 native-auth request: each concrete parameter type knows its target URL, -/// HTTP method, body, body encoding and telemetry identity. `MSALNativeAuthV2RequestConfigurator` -/// turns any of these into a fully-configured `MSIDHttpRequest` that reuses the shared AAD request -/// pipeline (device-id headers, PkeyAuth, correlation, server telemetry). -protocol MSALNativeAuthV2Requestable { - var context: MSALNativeAuthRequestContext { get } - var httpMethod: String { get } - var encoding: MSALNativeAuthUrlRequestEncoding { get } - var apiId: MSALNativeAuthTelemetryApiId { get } - var operationType: MSALNativeAuthOperationType { get } - /// `true` only for the `/token` endpoint, which returns a plain OAuth response (not HAL) and must - /// keep the default raw-JSON response serializer instead of the HAL serializer. - var expectsRawJSONResponse: Bool { get } - var body: [AnyHashable: Any] { get } - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL -} - -extension MSALNativeAuthV2Requestable { - var httpMethod: String { "POST" } - var expectsRawJSONResponse: Bool { false } -} - -/// The destination of a V2 request: either a well-known endpoint or a server-provided HAL `href`. -enum MSALNativeAuthV2RequestTarget { - case endpoint(MSALNativeAuthV2Endpoint) - case href(String) - - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { - switch self { - case .endpoint(let endpoint): - return try resolver.url(for: endpoint) - case .href(let href): - return try resolver.url(forHref: href) - } - } -} - -/// `POST /authorize/challenge` (the authorization challenge that starts a flow). Sends ONLY `client_id` (form encoded). -struct MSALNativeAuthV2AuthorizeChallengeStartParameters: MSALNativeAuthV2Requestable { - let context: MSALNativeAuthRequestContext - let clientId: String - let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded - let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge - let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeStart.rawValue - - var body: [AnyHashable: Any] { - return ["client_id": clientId] - } - - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { - return try resolver.url(for: .authorizeChallenge) - } -} - -/// `POST /authorize/challenge` resume. Sends ONLY `continuation_token` (form encoded). -struct MSALNativeAuthV2AuthorizeChallengeContinueParameters: MSALNativeAuthV2Requestable { - let context: MSALNativeAuthRequestContext - let continuationToken: String - let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded - let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge - let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeContinue.rawValue - - var body: [AnyHashable: Any] { - return ["continuation_token": continuationToken] - } - - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { - return try resolver.url(for: .authorizeChallenge) - } -} - -/// `POST /token` authorization-code exchange. Form encoded, raw OAuth (non-HAL) response. Includes -/// `client_info=true` so ESTS returns the `client_info` blob required to persist tokens to the cache. -struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { - let context: MSALNativeAuthRequestContext - let clientId: String - let code: String - let scopes: [String] - let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded - let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2Token - let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.token.rawValue - let expectsRawJSONResponse = true - - var body: [AnyHashable: Any] { - var form: [AnyHashable: Any] = [ - "grant_type": "authorization_code", - "code": code, - "client_id": clientId, - MSALNativeAuthRequestParametersKey.clientInfo.rawValue: true.description - ] - if !scopes.isEmpty { - form["scope"] = scopes.joined(separator: " ") - } - return form - } - - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { - return try resolver.url(for: .token) - } -} - -/// The signup/signin/resetpassword `start` entry requests. JSON encoded `{username, continuationToken}`, -/// targeting either the well-known start endpoint or a server-provided `href`. -struct MSALNativeAuthV2EntryParameters: MSALNativeAuthV2Requestable { - let context: MSALNativeAuthRequestContext - let target: MSALNativeAuthV2RequestTarget - let apiId: MSALNativeAuthTelemetryApiId - let operationType: MSALNativeAuthOperationType - let username: String - let continuationToken: String - let encoding: MSALNativeAuthUrlRequestEncoding = .json - - var body: [AnyHashable: Any] { - return ["username": username, "continuationToken": continuationToken] - } - - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { - return try target.url(resolver: resolver) - } -} - -/// A HAL follow-up request driven by a server-provided `href` (challenge, verify, submit*, register, -/// update-password, poll). JSON encoded with a caller-supplied body. -struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { - let context: MSALNativeAuthRequestContext - let href: String - let httpMethod: String - let apiId: MSALNativeAuthTelemetryApiId - let operationType: MSALNativeAuthOperationType - let body: [AnyHashable: Any] - let encoding: MSALNativeAuthUrlRequestEncoding = .json - - func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { - return try resolver.url(forHref: href) - } -} - -// MARK: - Request configurator - /// Builds a fully-configured `MSIDHttpRequest` for any `MSALNativeAuthV2Requestable`. Subclassing /// `MSIDAADRequestConfigurator` gives V2 the standard device-id (`x-client-*`) headers, app metadata, /// PkeyAuth, `Accept: application/json`, correlation headers and the authority network-host rewrite. diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift new file mode 100644 index 0000000000..ea438a826f --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// `POST /authorize/challenge` resume. Sends ONLY `continuation_token` (form encoded). +struct MSALNativeAuthV2AuthorizeChallengeContinueParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let continuationToken: String + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeContinue.rawValue + + var body: [AnyHashable: Any] { + return [MSALNativeAuthRequestParametersKey.continuationToken.rawValue: continuationToken] + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(for: .authorizeChallenge) + } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift new file mode 100644 index 0000000000..e28e5c0854 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// `POST /authorize/challenge` (the authorization challenge that starts a flow). Sends ONLY `client_id` (form encoded). +struct MSALNativeAuthV2AuthorizeChallengeStartParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let clientId: String + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeStart.rawValue + + var body: [AnyHashable: Any] { + return [MSALNativeAuthRequestParametersKey.clientId.rawValue: clientId] + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(for: .authorizeChallenge) + } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift new file mode 100644 index 0000000000..6ad97cf84c --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The signup/signin/resetpassword `start` entry requests. JSON encoded `{username, continuationToken}`, +/// targeting either the well-known start endpoint or a server-provided `href`. +struct MSALNativeAuthV2EntryParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let target: MSALNativeAuthV2RequestTarget + let apiId: MSALNativeAuthTelemetryApiId + let operationType: MSALNativeAuthOperationType + let username: String + let continuationToken: String + let encoding: MSALNativeAuthUrlRequestEncoding = .json + + var body: [AnyHashable: Any] { + return ["username": username, "continuationToken": continuationToken] + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try target.url(resolver: resolver) + } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift new file mode 100644 index 0000000000..009002eeec --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// A HAL follow-up request driven by a server-provided `href` (challenge, verify, submit*, register, +/// update-password, poll). JSON encoded with a caller-supplied body. +struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let href: String + let httpMethod: String + let apiId: MSALNativeAuthTelemetryApiId + let operationType: MSALNativeAuthOperationType + let body: [AnyHashable: Any] + let encoding: MSALNativeAuthUrlRequestEncoding = .json + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(forHref: href) + } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift new file mode 100644 index 0000000000..bdd366441d --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The destination of a V2 request: either a well-known endpoint or a server-provided HAL `href`. +enum MSALNativeAuthV2RequestTarget { + case endpoint(MSALNativeAuthV2Endpoint) + case href(String) + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + switch self { + case .endpoint(let endpoint): + return try resolver.url(for: endpoint) + case .href(let href): + return try resolver.url(forHref: href) + } + } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2Requestable.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2Requestable.swift new file mode 100644 index 0000000000..ceb0f6433f --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2Requestable.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Describes a single V2 native-auth request: each concrete parameter type knows its target URL, +/// HTTP method, body, body encoding and telemetry identity. `MSALNativeAuthV2RequestConfigurator` +/// turns any of these into a fully-configured `MSIDHttpRequest` that reuses the shared AAD request +/// pipeline (device-id headers, PkeyAuth, correlation, server telemetry). +protocol MSALNativeAuthV2Requestable { + var context: MSALNativeAuthRequestContext { get } + var httpMethod: String { get } + var encoding: MSALNativeAuthUrlRequestEncoding { get } + var apiId: MSALNativeAuthTelemetryApiId { get } + var operationType: MSALNativeAuthOperationType { get } + /// `true` only for the `/token` endpoint, which returns a plain OAuth response (not HAL) and must + /// keep the default raw-JSON response serializer instead of the HAL serializer. + var expectsRawJSONResponse: Bool { get } + var body: [AnyHashable: Any] { get } + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL +} + +extension MSALNativeAuthV2Requestable { + var httpMethod: String { "POST" } + var expectsRawJSONResponse: Bool { false } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift new file mode 100644 index 0000000000..427c30f350 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// `POST /token` authorization-code exchange. Form encoded, raw OAuth (non-HAL) response. Includes +/// `client_info=true` so ESTS returns the `client_info` blob required to persist tokens to the cache. +struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let clientId: String + let code: String + let scopes: [String] + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2Token + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.token.rawValue + let expectsRawJSONResponse = true + + var body: [AnyHashable: Any] { + var form: [AnyHashable: Any] = [ + MSALNativeAuthRequestParametersKey.grantType.rawValue: "authorization_code", + "code": code, + MSALNativeAuthRequestParametersKey.clientId.rawValue: clientId, + MSALNativeAuthRequestParametersKey.clientInfo.rawValue: true.description + ] + if !scopes.isEmpty { + form[MSALNativeAuthRequestParametersKey.scope.rawValue] = scopes.joined(separator: " ") + } + return form + } + + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { + return try resolver.url(for: .token) + } +} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift index a8ad54a2d5..844e49a4b8 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift @@ -28,7 +28,7 @@ import Foundation /// /// In V2 the server drives the flow: at each step the SDK reports a concrete /// ``MSALNativeAuthState`` subclass through its dedicated ``MSALNativeAuthFlowDelegate`` callback -/// (e.g. ``MSALNativeAuthFlowDelegate/onCodeRequired(state:)``). The app then continues the flow by +/// (e.g. ``MSALNativeAuthCodeRequiredDelegate/onCodeRequired(state:scenario:)``). The app then continues the flow by /// calling the method(s) exposed on that concrete state — each state exposes only the /// continuations valid for its step, so invalid calls are impossible. /// @@ -44,19 +44,19 @@ public class MSALNativeAuthState: NSObject { /// it. Internal detail — not part of the public API surface. var scenario: MSALNativeAuthFlowScenario = .unknown - /// The flow engine that continues the server-driven flow from this state, injected by the SDK + /// The internal state that continues the server-driven flow from this state, injected by the SDK /// when the state is created. Internal detail — not part of the public API surface. `nil` only for /// states an app constructs directly (which cannot advance a flow). - var engine: MSALNativeAuthFlowState? + var internalState: MSALNativeAuthFlowInternalState? - /// Forwards a continuation operation to the flow engine. If the state has no engine (e.g. it was - /// constructed directly by the app rather than handed back by the SDK), the delegate is notified - /// with a general error instead of silently doing nothing. + /// Forwards a continuation operation to the internal state. If the state has no internal state + /// (e.g. it was constructed directly by the app rather than handed back by the SDK), the delegate + /// is notified with a general error instead of silently doing nothing. func run( delegate: MSALNativeAuthFlowDelegate, - operation: @escaping (MSALNativeAuthV2FlowControlling, MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse + operation: @escaping (MSALNativeAuthFlowControlling, MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse ) { - guard let engine = engine else { + guard let internalState = internalState else { Task { @MainActor in delegate.onFlowError( error: MSALNativeAuthFlowError( @@ -68,6 +68,6 @@ public class MSALNativeAuthState: NSObject { } return } - engine.run(delegate: delegate, operation: operation) + internalState.run(delegate: delegate, operation: operation) } } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift similarity index 97% rename from MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift rename to MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 96b123530f..5c8f387cc8 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthV2FlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -26,9 +26,9 @@ import XCTest @testable import MSAL @_implementationOnly import MSAL_Private -final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { +final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { - private var sut: MSALNativeAuthV2FlowController! + private var sut: MSALNativeAuthFlowController! private var requestProviderMock: MSALNativeAuthV2RequestProviderMock! private var validatorMock: MSALNativeAuthV2ResponseValidatorMock! private var cacheAccessorMock: MSALNativeAuthCacheAccessorMock! @@ -53,8 +53,8 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { // MARK: - Helpers - private func makeState(links: [String: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowState { - let continuation = MSALNativeAuthV2ContinuationState( + private func makeState(links: [String: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowInternalState { + let continuation = MSALNativeAuthFlowContinuationState( flowScenario: .passwordReset, continuationToken: continuationToken, links: links, @@ -62,7 +62,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { sentToHint: "u***@contoso.com", codeLength: 8 ) - return MSALNativeAuthFlowState(continuation: continuation, controller: sut) + return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) } private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParametersV2 { @@ -270,8 +270,8 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { links: [String: URL], authMethods: [MSALAuthMethod] = [], continuationToken: String = "ct" - ) -> MSALNativeAuthFlowState { - let continuation = MSALNativeAuthV2ContinuationState( + ) -> MSALNativeAuthFlowInternalState { + let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, continuationToken: continuationToken, links: links, @@ -280,7 +280,7 @@ final class MSALNativeAuthV2FlowControllerTests: MSALNativeAuthTestCase { codeLength: 8, authMethods: authMethods ) - return MSALNativeAuthFlowState(continuation: continuation, controller: sut) + return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) } func test_signUp_happyPath_returnsCodeRequired() async { diff --git a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift index 29f461b9a1..0e99b0a80d 100644 --- a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift +++ b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift @@ -80,7 +80,7 @@ class MSALNativeAuthControllerFactoryMock: MSALNativeAuthControllerBuildable { var jitController = MSALNativeAuthJITControllerMock() var resetPasswordController = MSALNativeAuthResetPasswordControllerMock() var credentialsController = MSALNativeAuthCredentialsControllerMock() - var v2FlowController = MSALNativeAuthV2FlowControllerMock() + var v2FlowController = MSALNativeAuthFlowControllerMock() func makeSignUpController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthSignUpControlling { return signUpController @@ -102,7 +102,7 @@ class MSALNativeAuthControllerFactoryMock: MSALNativeAuthControllerBuildable { return credentialsController } - func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthV2FlowControlling { + func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { return v2FlowController } } @@ -114,14 +114,14 @@ class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuild var jitController: MSALNativeAuthJITControlling! var resetPasswordController: MSALNativeAuthResetPasswordControlling! var credentialsController: MSALNativeAuthCredentialsControlling! - var v2FlowController: MSALNativeAuthV2FlowControlling! + var v2FlowController: MSALNativeAuthFlowControlling! init (signUpController: MSALNativeAuthSignUpControlling = MSALNativeAuthSignUpControllerMock(), signInController: MSALNativeAuthSignInControlling = MSALNativeAuthSignInControllerMock(), jitController: MSALNativeAuthJITControlling = MSALNativeAuthJITControllerMock(), resetPasswordController: MSALNativeAuthResetPasswordControlling = MSALNativeAuthResetPasswordControllerMock(), credentialsController: MSALNativeAuthCredentialsControlling = MSALNativeAuthCredentialsControllerMock(), - v2FlowController: MSALNativeAuthV2FlowControlling = MSALNativeAuthV2FlowControllerMock()) { + v2FlowController: MSALNativeAuthFlowControlling = MSALNativeAuthFlowControllerMock()) { self.signUpController = signUpController self.signInController = signInController self.jitController = jitController @@ -150,7 +150,7 @@ class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuild return credentialsController } - func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthV2FlowControlling { + func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { return v2FlowController } } diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift similarity index 62% rename from MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift rename to MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift index 528cc4b07c..adaffe666a 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2FlowControllerMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift @@ -25,68 +25,68 @@ import Foundation @testable import MSAL -class MSALNativeAuthV2FlowControllerMock: MSALNativeAuthV2FlowControlling { +class MSALNativeAuthFlowControllerMock: MSALNativeAuthFlowControlling { var correlationId = UUID() - var resetPasswordResponse: MSALNativeAuthV2FlowControllerResponse? - var signUpResponse: MSALNativeAuthV2FlowControllerResponse? - var signInResponse: MSALNativeAuthV2FlowControllerResponse? - var submitCodeResponse: MSALNativeAuthV2FlowControllerResponse? - var submitPasswordResponse: MSALNativeAuthV2FlowControllerResponse? - var submitNewPasswordResponse: MSALNativeAuthV2FlowControllerResponse? - var submitAttributesResponse: MSALNativeAuthV2FlowControllerResponse? - var selectAuthMethodResponse: MSALNativeAuthV2FlowControllerResponse? - var submitChallengeResponse: MSALNativeAuthV2FlowControllerResponse? - var resendCodeResponse: MSALNativeAuthV2FlowControllerResponse? + var resetPasswordResponse: MSALNativeAuthFlowControllerResponse? + var signUpResponse: MSALNativeAuthFlowControllerResponse? + var signInResponse: MSALNativeAuthFlowControllerResponse? + var submitCodeResponse: MSALNativeAuthFlowControllerResponse? + var submitPasswordResponse: MSALNativeAuthFlowControllerResponse? + var submitNewPasswordResponse: MSALNativeAuthFlowControllerResponse? + var submitAttributesResponse: MSALNativeAuthFlowControllerResponse? + var selectAuthMethodResponse: MSALNativeAuthFlowControllerResponse? + var submitChallengeResponse: MSALNativeAuthFlowControllerResponse? + var resendCodeResponse: MSALNativeAuthFlowControllerResponse? - private func notImplementedResponse() -> MSALNativeAuthV2FlowControllerResponse { - return MSALNativeAuthV2FlowControllerResponse( + private func notImplementedResponse() -> MSALNativeAuthFlowControllerResponse { + return MSALNativeAuthFlowControllerResponse( .error(error: MSALNativeAuthFlowError(type: .notImplemented), newState: nil), correlationId: correlationId ) } - func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthV2FlowControllerResponse { + func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthFlowControllerResponse { return resetPasswordResponse ?? notImplementedResponse() } - func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthV2FlowControllerResponse { + func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthFlowControllerResponse { return signUpResponse ?? notImplementedResponse() } - func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthV2FlowControllerResponse { + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse { return signInResponse ?? notImplementedResponse() } - func submitCode(_ code: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { return submitCodeResponse ?? notImplementedResponse() } - func submitPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { return submitPasswordResponse ?? notImplementedResponse() } - func submitNewPassword(_ password: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { return submitNewPasswordResponse ?? notImplementedResponse() } - func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { return submitAttributesResponse ?? notImplementedResponse() } func selectAuthMethod( _ method: MSALAuthMethod, verificationContact: String?, - state: MSALNativeAuthFlowState - ) async -> MSALNativeAuthV2FlowControllerResponse { + state: MSALNativeAuthFlowInternalState + ) async -> MSALNativeAuthFlowControllerResponse { return selectAuthMethodResponse ?? notImplementedResponse() } - func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { return submitChallengeResponse ?? notImplementedResponse() } - func resendCode(state: MSALNativeAuthFlowState) async -> MSALNativeAuthV2FlowControllerResponse { + func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { return resendCodeResponse ?? notImplementedResponse() } } diff --git a/azure_pipelines/broker_build_steps.yml b/azure_pipelines/broker_build_steps.yml new file mode 100644 index 0000000000..ca679de1c1 --- /dev/null +++ b/azure_pipelines/broker_build_steps.yml @@ -0,0 +1,149 @@ +# ============================================================================= +# Broker repo-specific build steps (checkouts, submodules, CocoaPods, build). +# +# These are the steps that are unique to the Broker submodule validation. The +# generic macOS tool setup (pool, Xcode, Ruby gems, CocoaPods CLI, xcbeautify) +# is intentionally NOT here — it is provided by the shared ACES macOS job +# template (Pipeline YAMLs/shared/aces-macos-job.yml@pipelinesShared), which +# injects these steps after the tool setup. +# +# Parameters: +# target build.py target (e.g. ios_library, mac_library). +# ============================================================================= +parameters: + - name: target + type: string + +steps: + - checkout: azure-activedirectory-tokenbroker-for-objc + displayName: 'Checkout Broker' + clean: false + submodules: false + fetchTags: true + persistCredentials: true + + - checkout: self + displayName: 'Checkout MSAL' + clean: false + submodules: false + fetchTags: true + path: 's/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc' + persistCredentials: true + + - task: Bash@3 + displayName: 'Checkout MSAL submodules + ADAL' + inputs: + workingDirectory: $(Pipeline.Workspace)/s + targetType: 'inline' + script: | + cd azure-activedirectory-tokenbroker-for-objc + git submodule update --init --recursive ADAuthenticationBroker/Frameworks/adal + cd ADAuthenticationBroker/Frameworks/microsoft-authentication-library-for-objc + git submodule update --init --recursive + + - checkout: WorkplaceJoin-for-iOS + displayName: 'Checkout WPJ' + clean: false + submodules: false + fetchTags: true + path: 's/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/WorkplaceJoin-for-iOS' + persistCredentials: true + + - task: AzureCLI@2 + inputs: + azureSubscription: 'AuthSdkResourceManager' + scriptType: 'pscore' + scriptLocation: 'inlineScript' + inlineScript: | + # if this fails, check out this bash script that includes diagnostics: + # https://gist.github.com/johnterickson/19f80a3e969e39f1000d118739176e62 + # uncomment these for more debugging spew + # GIT_TRACE=1 + # GIT_CURL_VERBOSE=1 + + # Note that the resource is specified to limit the token to Azure DevOps + $token = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv + Write-Host "##vso[task.setvariable variable=aadToken;issecret=true]$token" + + - task: Bash@3 + displayName: 'Checkout NGC Submodules' + env: + AccessToken: $(MSAzureToken_encoded) + inputs: + workingDirectory: $(Pipeline.Workspace)/s + targetType: 'inline' + script: | + cd azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks + git -c http.https://msazure.visualstudio.com/DefaultCollection/One/_git/AD-MFA-NGCAuthentication.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init AD-MFA-NGCAuthentication + cd AD-MFA-NGCAuthentication + git -c http.https://msazure.visualstudio.com/DefaultCollection/One/_git/AD-MFA-NGCKeyProvider-ios.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init NGCKeyProvider + git -c http.https://msazure.visualstudio.com/DefaultCollection/One/_git/AD-MFA-MSAuthNetworking.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init MSAuthNetworking + + - task: Bash@3 + displayName: 'Checkout WPJ openssl-msft submodule' + inputs: + workingDirectory: $(Pipeline.Workspace)/s + targetType: 'inline' + script: | + cd azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/WorkplaceJoin-for-iOS + git -c http.https://msazure.visualstudio.com/DefaultCollection/PlatformCrypto/_git/openssl-msft.extraheader="AUTHORIZATION: bearer $(aadToken)" submodule update --init Frameworks/openssl-msft + + - task: Bash@3 + displayName: 'Update WPJ submodules' + inputs: + workingDirectory: $(Pipeline.Workspace)/s + targetType: 'inline' + script: | + cd azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Frameworks/WorkplaceJoin-for-iOS + git submodule update --init --recursive Frameworks/microsoft-authentication-library-for-objc + + - ${{ if eq(parameters.target, 'mac_library') }}: + - task: Cache@2 + displayName: 'Cache CocoaPods' + inputs: + key: 'cocoapods | "$(Agent.OS)" | azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Podfile.lock' + path: '$(Pipeline.Workspace)/s/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker/Pods' + + - task: Bash@3 + displayName: 'Install CocoaPods dependencies' + env: + AAD_TOKEN: $(aadToken) + inputs: + workingDirectory: $(Pipeline.Workspace)/s/azure-activedirectory-tokenbroker-for-objc/ADAuthenticationBroker + targetType: 'inline' + script: | + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0=http.https://office.visualstudio.com/.extraheader \ + GIT_CONFIG_VALUE_0="AUTHORIZATION: bearer $AAD_TOKEN" \ + pod install + retryCountOnTaskFailure: 1 + + - task: UsePythonVersion@0 + displayName: 'Use Python 3.x' + + - task: Bash@3 + displayName: 'Run a python script for Broker' + inputs: + targetType: 'inline' + script: | + cd azure-activedirectory-tokenbroker-for-objc + echo "executing build:./build.py" + { output=$(./build.py --show-build-settings --target ${{ parameters.target }} 2>&1 1>&3-) ;} 3>&1 + final_status=$(<./build/status.txt) + echo "FINAL STATUS = ${final_status}" + echo "POSSIBLE ERRORS: ${output}" + + if [ $final_status != "0" ]; then + echo "Build & Testing Failed! \n ${output}" >&2 + fi + failOnStderr: true + + - task: Bash@3 + condition: always() + displayName: Cleanup + inputs: + targetType: 'inline' + script: | + rm -rf ./build/status.txt From b4a4500a986fb56a3a05fca0a1b052636985b3a5 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 15:30:37 +0100 Subject: [PATCH 28/85] Refactor V2 native-auth to use typed key enums (V1-style) Replace scattered string literals in the Native Auth V2 (HAL) request/response paths with pure String-backed enums, mirroring V1's MSALNativeAuthRequestParametersKey style (cases only, no static helpers). Wire format is preserved byte-for-byte. - Add MSALNativeAuthV2RequestBodyKey (camelCase HAL body keys) - Add MSALNativeAuthV2LinkRelation (HAL _links relation names) - Add MSALNativeAuthV2HALAction (the response `action` verb) - Route request bodies, link lookups, and the validator switch through the enums - Split per-method links into a typed methodLinks collection on the continuation state (drop the composite "method:" key) - Trim comments that merely restate the code - Fix pre-existing test-target breaks (mock param types, flow scenario case) - Add build/test guidelines to .clinerules/AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .clinerules/AGENTS.md | 4 ++ MSAL/MSAL.xcodeproj/project.pbxproj | 18 +++++++ .../MSALNativeAuthFlowContinuationState.swift | 11 ++-- .../v2/MSALNativeAuthFlowController.swift | 54 ++++++++++--------- .../v2/MSALNativeAuthHALResponse.swift | 17 ++++++ .../v2/MSALNativeAuthV2HALAction.swift | 40 ++++++++++++++ .../MSALNativeAuthV2ResponseValidator.swift | 54 +++++++++---------- .../v2/MSALNativeAuthV2LinkRelation.swift | 43 +++++++++++++++ .../v2/MSALNativeAuthV2RequestBodyKey.swift | 41 ++++++++++++++ .../v2/MSALNativeAuthV2RequestProvider.swift | 33 ++++++++---- .../MSALNativeAuthV2EntryParameters.swift | 5 +- .../MSALNativeAuthV2TokenParameters.swift | 2 +- .../v2/MSALNativeAuthFlowControllerMock.swift | 4 +- ...ALNativeAuthV2ResponseValidatorTests.swift | 2 +- 14 files changed, 257 insertions(+), 71 deletions(-) create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift diff --git a/.clinerules/AGENTS.md b/.clinerules/AGENTS.md index aaee01fe71..1e0a39c8a2 100644 --- a/.clinerules/AGENTS.md +++ b/.clinerules/AGENTS.md @@ -26,6 +26,10 @@ When creating a new application with MSAL authentication, users need to select a | **Authority Endpoint** | Uses tenant ID or common | Uses tenant subdomain | | **Use Cases** | Enterprise apps, B2E scenarios | Consumer apps, B2C scenarios | +## Build and test guidelines + +AI agents MUST build and run tests using the build/test configuration already set up in Xcode — i.e. the schemes defined in `MSAL.xcworkspace`, driven through `build.py` (e.g. `./build.py --targets iosFramework macFramework`). Always use `MSAL.xcworkspace`, never open or build `MSAL.xcodeproj` directly, and do not invent ad-hoc `xcodebuild` invocations, schemes, or configurations that diverge from the ones configured in the workspace. If a build/test run needs a specific simulator, select an available one via the `IOS_SIM_DEVICE` / `IOS_SIM_OS` environment variables (consumed by `build.py`) rather than changing the scheme or configuration. + ## MSAL API usage Sample code snippets for both Swift & Objective-C can be found in the file `.clinerules/03-MSAL-API-usage.md` diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 68bfbfc5cb..e52c94664f 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -119,6 +119,8 @@ 12E2160C2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; + F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; + D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; 1E1A2E042256D12F001009ED /* MSALTestAppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64B01E5AAC5C0086D120 /* MSALTestAppSettings.m */; }; @@ -421,6 +423,8 @@ 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; + 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; + 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; @@ -449,6 +453,7 @@ 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + 2161D7C3F3059052DD18D048 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; 7207E63A2FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -1026,6 +1031,7 @@ B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; 081C1B43CDAC5F4990EA68FB /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; @@ -2309,6 +2315,8 @@ 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenRequestHandling.swift; sourceTree = ""; }; 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; + 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2LinkRelation.swift; sourceTree = ""; }; + 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBodyKey.swift; sourceTree = ""; }; 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthRegistrationRequiredState.swift; sourceTree = ""; }; 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFAVerificationRequiredState.swift; sourceTree = ""; }; 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthNewPasswordRequiredState.swift; sourceTree = ""; }; @@ -2388,6 +2396,7 @@ 963377BE211E14C600943EE0 /* MSALWebviewType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWebviewType.m; sourceTree = ""; }; 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALResponse.swift; sourceTree = ""; }; + 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALAction.swift; sourceTree = ""; }; 9648AF54225D826500F66801 /* MSALTelemetryConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTelemetryConfig+Internal.h"; sourceTree = ""; }; 9648AF5B225DD6A900F66801 /* MSALGlobalConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALGlobalConfig+Internal.h"; sourceTree = ""; }; 9682A62A218290F700E37E63 /* MSALDefinitions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDefinitions.h; sourceTree = ""; }; @@ -3197,6 +3206,8 @@ isa = PBXGroup; children = ( 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */, + 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */, + 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */, F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */, B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */, D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */, @@ -3629,6 +3640,7 @@ isa = PBXGroup; children = ( 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */, + 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */, FADE0000000000000000AA01 /* HALResource.swift */, C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */, 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */, @@ -7462,12 +7474,15 @@ 01F6FDA46510AF671264602E /* MSALNativeAuthFlowControlling.swift in Sources */, 8E0486CA55F25C1987E4067A /* MSALNativeAuthFlowController.swift in Sources */, CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */, + 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */, FADE0000000000000000AA02 /* HALResource.swift in Sources */, 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseValidator.swift in Sources */, 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */, + F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */, + D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, @@ -7793,12 +7808,15 @@ E68C311BD4DDECABFAA212FD /* MSALNativeAuthFlowControlling.swift in Sources */, 76EEE63606562E71DCFDA606 /* MSALNativeAuthFlowController.swift in Sources */, 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */, + 2161D7C3F3059052DD18D048 /* MSALNativeAuthV2HALAction.swift in Sources */, FADE0000000000000000AA03 /* HALResource.swift in Sources */, 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */, 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */, + 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */, + 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift index 6e92db73a7..8ec648656a 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -32,8 +32,9 @@ struct MSALNativeAuthFlowContinuationState { let flowScenario: MSALNativeAuthFlowScenario let continuationToken: String /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", - /// "challenge", "enroll", "activate", "submitAttributes"). Per-method links are keyed "method:". + /// "challenge", "enroll", "activate", "submitAttributes"). let links: [String: URL] + let methodLinks: [String: URL] let username: String? let sentToHint: String? let codeLength: Int? @@ -57,6 +58,7 @@ struct MSALNativeAuthFlowContinuationState { flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, links: [String: URL], + methodLinks: [String: URL] = [:], username: String?, sentToHint: String? = nil, codeLength: Int? = nil, @@ -68,6 +70,7 @@ struct MSALNativeAuthFlowContinuationState { self.flowScenario = flowScenario self.continuationToken = continuationToken self.links = links + self.methodLinks = methodLinks self.username = username self.sentToHint = sentToHint self.codeLength = codeLength @@ -77,12 +80,12 @@ struct MSALNativeAuthFlowContinuationState { self.signUpAutofillSubmittedIds = signUpAutofillSubmittedIds } - func link(_ relation: String) -> URL? { - return links[relation] + func link(_ relation: MSALNativeAuthV2LinkRelation) -> URL? { + return links[relation.rawValue] } /// The challenge / enroll link associated with a specific auth method. func methodLink(for methodId: String) -> URL? { - return links["method:\(methodId)"] + return methodLinks[methodId] } } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 4e5ac0b478..c87d1127c6 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -151,7 +151,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati chosen = otpMethod ?? passwordMethod } - guard let method = chosen, let href = method.links["challenge"] else { + guard let method = chosen, let href = method.link(for: .challenge) else { return failure( .error( MSALNativeAuthFlowError( @@ -257,7 +257,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let context = MSALNativeAuthRequestContext(correlationId: nil) let continuation = state.continuation - guard let verifyHref = continuation.link("verify")?.absoluteString else { + guard let verifyHref = continuation.link(.verify)?.absoluteString else { let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), @@ -296,7 +296,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( continuation.flowScenario, continuationToken: token, - links: ["update": updateHref], + links: [.update: updateHref], username: continuation.username, scopes: continuation.scopes ) @@ -323,7 +323,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignInSubmitPassword, context: context) let continuation = state.continuation - guard let verifyHref = continuation.link("verify")?.absoluteString else { + guard let verifyHref = continuation.link(.verify)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), event: event, @@ -356,7 +356,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmit, context: context) let continuation = state.continuation - guard let updateHref = continuation.link("update")?.absoluteString else { + guard let updateHref = continuation.link(.update)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing update link")), event: event, @@ -432,7 +432,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpSubmitAttributes, context: context) let continuation = state.continuation - guard let submitHref = continuation.link("submitAttributes")?.absoluteString else { + guard let submitHref = continuation.link(.submitAttributes)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing submit-attributes link")), event: event, @@ -471,8 +471,8 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let continuation = state.continuation // JIT (strong-auth registration) carries an `enroll` link; MFA carries a `challenge` link. - if continuation.link("enroll") != nil { - guard let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link("enroll"))?.absoluteString else { + if continuation.link(.enroll) != nil { + guard let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link(.enroll))?.absoluteString else { let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) return failure( .error( @@ -503,7 +503,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati context: context ) } else { - guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link("challenge"))?.absoluteString else { + guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link(.challenge))?.absoluteString else { let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) return failure( .error( @@ -526,7 +526,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( continuation.flowScenario, continuationToken: token, - links: ["verify": verifyHref, "resend": resendHref], + links: [.verify: verifyHref, .resend: resendHref], username: continuation.username, sentToHint: sentTo.isEmpty ? continuation.sentToHint : sentTo, codeLength: codeLength, @@ -559,10 +559,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MFA uses the `verify` link and submits the code via the `otp` field. let submitHref: String let isActivation: Bool - if let activateHref = continuation.link("activate")?.absoluteString { + if let activateHref = continuation.link(.activate)?.absoluteString { submitHref = activateHref isActivation = true - } else if let verifyHref = continuation.link("verify")?.absoluteString { + } else if let verifyHref = continuation.link(.verify)?.absoluteString { submitHref = verifyHref isActivation = false } else { @@ -606,7 +606,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordResendCode, context: context) let continuation = state.continuation - guard let resendHref = continuation.link("resend")?.absoluteString else { + guard let resendHref = continuation.link(.resend)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing resend link")), event: event, @@ -689,7 +689,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["verify": verifyHref, "resend": resendHref], + links: [.verify: verifyHref, .resend: resendHref], username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, @@ -739,7 +739,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["verify": verifyHref, "resend": resendHref], + links: [.verify: verifyHref, .resend: resendHref], username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, @@ -757,7 +757,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["verify": verifyHref], + links: [.verify: verifyHref], username: username, scopes: scopes, signUpAutofillValues: signUpAutofillValues, @@ -769,7 +769,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["update": updateHref], + links: [.update: updateHref], username: username, scopes: scopes, signUpAutofillValues: signUpAutofillValues, @@ -801,7 +801,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let submitState = makeState( flowScenario, continuationToken: token, - links: ["submitAttributes": submitHref], + links: [.submitAttributes: submitHref], username: username, scopes: scopes, signUpAutofillValues: signUpAutofillValues, @@ -813,7 +813,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["submitAttributes": submitHref], + links: [.submitAttributes: submitHref], username: username, scopes: scopes, signUpAutofillValues: signUpAutofillValues, @@ -829,7 +829,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["challenge": challengeHref], + links: [.challenge: challengeHref], username: username, authMethods: authMethods, methodLinks: methodLinks, @@ -844,7 +844,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["enroll": enrollHref], + links: [.enroll: enrollHref], username: username, authMethods: authMethods, methodLinks: methodLinks, @@ -858,7 +858,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let newState = makeState( flowScenario, continuationToken: token, - links: ["activate": activateHref], + links: [.activate: activateHref], username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, @@ -989,7 +989,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private func makeState( _ flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, - links: [String: String?], + links: [MSALNativeAuthV2LinkRelation: String?], username: String?, sentToHint: String? = nil, codeLength: Int? = nil, @@ -1003,18 +1003,20 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati var resolvedLinks: [String: URL] = [:] for (relation, href) in links { if let href = href, let url = try? resolver.url(forHref: href) { - resolvedLinks[relation] = url + resolvedLinks[relation.rawValue] = url } } + var resolvedMethodLinks: [String: URL] = [:] for (methodId, href) in methodLinks { if let url = try? resolver.url(forHref: href) { - resolvedLinks["method:\(methodId)"] = url + resolvedMethodLinks[methodId] = url } } let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, continuationToken: continuationToken, links: resolvedLinks, + methodLinks: resolvedMethodLinks, username: username, sentToHint: sentToHint, codeLength: codeLength, @@ -1043,7 +1045,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati channelTargetType: MSALNativeAuthChannelType(value: type), loginHint: method.hint )) - if let link = method.links["challenge"] ?? method.links["enroll"] ?? method.links["register"] { + if let link = method.link(for: .challenge) ?? method.link(for: .enroll) ?? method.link(for: .register) { methodLinks[id] = link } } diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 2a26a45e0b..3ed8e4c095 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -42,6 +42,10 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let hint: String? /// `_links` of the embedded method, keyed by relation (e.g. "challenge", "verify"), value is the raw href. let links: [String: String] + + func link(for relation: MSALNativeAuthV2LinkRelation) -> String? { + return links[relation.rawValue] + } } /// An attribute the server requires during sign up (`collectAttributes` action). @@ -92,7 +96,20 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let error: ServerError? + /// The typed `action` this response instructs the SDK to perform, or `nil` when absent/unknown. + var halAction: MSALNativeAuthV2HALAction? { + return action.flatMap(MSALNativeAuthV2HALAction.init(rawValue:)) + } + + var isReadyToComplete: Bool { + return state == "continue" + } + func href(forRelation relation: String) -> String? { return links[relation] } + + func href(for relation: MSALNativeAuthV2LinkRelation) -> String? { + return links[relation.rawValue] + } } diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift new file mode 100644 index 0000000000..e11288bbe5 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// The `action` a Native Auth V2 (HAL) interaction response instructs the SDK to perform next. +/// +/// The validator maps the raw `action` string carried by ``MSALNativeAuthHALResponse`` onto one of +/// these cases to decide the next step of the flow. +enum MSALNativeAuthV2HALAction: String { + case challenge + case verify + case enroll + case register + case activate + case collectAttributes + case update + case poll +} diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 0adb9767b9..c2cd3662b2 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -79,7 +79,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .error(flowError(from: error)) } - if response.state == "continue" { + if response.isReadyToComplete { guard let continuationToken = response.continuationToken else { return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in 'continue' response")) } @@ -95,11 +95,11 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .signInMethods(continuationToken: continuationToken, methods: response.methods) } - switch response.action { - case "challenge": + switch response.halAction { + case .challenge: let method = response.methods.first - guard let challengeHref = method?.links["challenge"] ?? response.href(forRelation: "challenge") else { - return missingLink("challenge") + guard let challengeHref = method?.link(for: .challenge) ?? response.href(for: .challenge) else { + return missingLink(.challenge) } // MFA required: the server sets challengeContext.authenticationFactor to "multiFactor" // and embeds the available second-factor methods. Surface them for method selection @@ -116,41 +116,41 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin challengeHref: challengeHref, hint: method?.hint ?? response.hint ) - case "verify": + case .verify: // After a password, a `challenge` link plus embedded methods means MFA is required. - if let challengeHref = response.href(forRelation: "challenge"), !response.methods.isEmpty { + if let challengeHref = response.href(for: .challenge), !response.methods.isEmpty { return .mfaRequired( continuationToken: continuationToken, methods: response.methods, challengeHref: challengeHref ) } - guard let verifyHref = response.href(forRelation: "verify") else { - return missingLink("verify") + guard let verifyHref = response.href(for: .verify) else { + return missingLink(.verify) } // An email/OOB method carries a hint and/or a code length; a password method does not. if (response.codeLength ?? 0) > 0 || response.hint != nil || response.methodType == "email" { return .codeRequired( continuationToken: continuationToken, verifyHref: verifyHref, - resendHref: response.href(forRelation: "resend"), + resendHref: response.href(for: .resend), sentTo: response.hint ?? "", codeLength: response.codeLength ?? 0 ) } return .passwordRequired(continuationToken: continuationToken, verifyHref: verifyHref) - case "enroll", "register": - guard let enrollHref = response.href(forRelation: "enroll") ?? response.href(forRelation: "register") else { - return missingLink("enroll") + case .enroll, .register: + guard let enrollHref = response.href(for: .enroll) ?? response.href(for: .register) else { + return missingLink(.enroll) } return .registrationRequired( continuationToken: continuationToken, enrollHref: enrollHref, methods: response.methods ) - case "activate": - guard let activateHref = response.href(forRelation: "activate") else { - return missingLink("activate") + case .activate: + guard let activateHref = response.href(for: .activate) else { + return missingLink(.activate) } return .activationRequired( continuationToken: continuationToken, @@ -158,26 +158,26 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin sentTo: response.hint ?? "", codeLength: response.codeLength ?? 0 ) - case "collectAttributes": - guard let submitHref = response.href(forRelation: "submitAttributes") else { - return missingLink("submitAttributes") + case .collectAttributes: + guard let submitHref = response.href(for: .submitAttributes) else { + return missingLink(.submitAttributes) } return .attributesRequired( continuationToken: continuationToken, attributes: response.attributes, submitHref: submitHref ) - case "update": - guard let updateHref = response.href(forRelation: "update") ?? response.href(forRelation: "self") else { - return missingLink("update") + case .update: + guard let updateHref = response.href(for: .update) ?? response.href(for: .self) else { + return missingLink(.update) } return .updateRequired( continuationToken: continuationToken, updateHref: updateHref ) - case "poll": - guard let pollHref = response.href(forRelation: "poll") else { - return missingLink("poll") + case .poll: + guard let pollHref = response.href(for: .poll) else { + return missingLink(.poll) } return .pollInProgress( continuationToken: continuationToken, @@ -207,10 +207,10 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin /// The server returned an action that requires a follow-up link, but that link is absent. /// Fail here rather than passing a missing href down to the next request. - private func missingLink(_ relation: String) -> MSALNativeAuthV2InteractionValidatedResponse { + private func missingLink(_ relation: MSALNativeAuthV2LinkRelation) -> MSALNativeAuthV2InteractionValidatedResponse { return .error(MSALNativeAuthFlowError( type: .generalError, - errorDescription: "Invalid interaction response: missing '\(relation)' link" + errorDescription: "Invalid interaction response: missing '\(relation.rawValue)' link" )) } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift new file mode 100644 index 0000000000..6579c41201 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// HAL `_links` relation names the SDK follows to advance a Native Auth V2 (server-driven) flow. +/// +/// Every href the SDK resolves is keyed by one of these relations (top-level `_links` or an +/// embedded method's `_links`). +enum MSALNativeAuthV2LinkRelation: String { + case challenge + case verify + case resend + case update + case poll + case `continue` + case enroll + case register + case activate + case submitAttributes + case `self` +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift new file mode 100644 index 0000000000..a3df6d8519 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// JSON body keys used by the Native Auth V2 (server-driven, HAL) requests. +/// +/// V2 HAL bodies are camelCase JSON (unlike the snake_case, form-encoded keys in +/// ``MSALNativeAuthRequestParametersKey`` used by the OAuth `/token` and `/authorize-challenge` +/// endpoints). Centralizing these keys keeps the request bodies free of scattered string literals. +enum MSALNativeAuthV2RequestBodyKey: String { + case username + case continuationToken + case password + case code + case otp + case newPassword + case attributes + case target +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 8071108ace..dcd499c970 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -184,7 +184,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.submitPassword.rawValue, - body: ["password": password, "continuationToken": continuationToken] + body: [ + MSALNativeAuthV2RequestBodyKey.password.rawValue: password, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken + ] )) } @@ -199,7 +202,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.submitCode.rawValue, - body: ["code": code, "continuationToken": continuationToken] + body: [ + MSALNativeAuthV2RequestBodyKey.code.rawValue: code, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken + ] )) } @@ -214,7 +220,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, - body: ["attributes": attributes, "continuationToken": continuationToken] + body: [ + MSALNativeAuthV2RequestBodyKey.attributes.rawValue: attributes, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken + ] )) } @@ -223,9 +232,9 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { continuationToken: String, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { - var body: [AnyHashable: Any] = ["continuationToken": continuationToken] + var body: [AnyHashable: Any] = [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] if let target = target { - body["target"] = target + body[MSALNativeAuthV2RequestBodyKey.target.rawValue] = target } return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, @@ -247,7 +256,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.challenge.rawValue, - body: ["continuationToken": continuationToken] + body: [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] )) } @@ -262,7 +271,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.verify.rawValue, - body: ["otp": otp, "continuationToken": continuationToken] + body: [ + MSALNativeAuthV2RequestBodyKey.otp.rawValue: otp, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken + ] )) } @@ -277,7 +289,10 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "PUT", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, - body: ["newPassword": newPassword, "continuationToken": continuationToken] + body: [ + MSALNativeAuthV2RequestBodyKey.newPassword.rawValue: newPassword, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken + ] )) } @@ -291,7 +306,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.poll.rawValue, - body: ["continuationToken": continuationToken] + body: [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] )) } diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift index 6ad97cf84c..aed026226c 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift @@ -36,7 +36,10 @@ struct MSALNativeAuthV2EntryParameters: MSALNativeAuthV2Requestable { let encoding: MSALNativeAuthUrlRequestEncoding = .json var body: [AnyHashable: Any] { - return ["username": username, "continuationToken": continuationToken] + return [ + MSALNativeAuthV2RequestBodyKey.username.rawValue: username, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken + ] } func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift index 427c30f350..41dd51f7e4 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift @@ -39,7 +39,7 @@ struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { var body: [AnyHashable: Any] { var form: [AnyHashable: Any] = [ MSALNativeAuthRequestParametersKey.grantType.rawValue: "authorization_code", - "code": code, + MSALNativeAuthV2RequestBodyKey.code.rawValue: code, MSALNativeAuthRequestParametersKey.clientId.rawValue: clientId, MSALNativeAuthRequestParametersKey.clientInfo.rawValue: true.description ] diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift index adaffe666a..235d94febb 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift @@ -46,11 +46,11 @@ class MSALNativeAuthFlowControllerMock: MSALNativeAuthFlowControlling { ) } - func resetPassword(parameters: MSALNativeAuthResetPasswordParameters) async -> MSALNativeAuthFlowControllerResponse { + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse { return resetPasswordResponse ?? notImplementedResponse() } - func signUp(parameters: MSALNativeAuthSignUpParameters) async -> MSALNativeAuthFlowControllerResponse { + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse { return signUpResponse ?? notImplementedResponse() } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 403b824579..8e2e63eda9 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -77,7 +77,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateAuthorizeChallenge_withContinuationToken() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .resetPassword) + let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .passwordReset) XCTAssertEqual(result, .continuationToken(continuationToken: "ct", href: "https://contoso.com/reset")) } From d9ee86bb83394a4322b2134b77f716abe5b490f8 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 16:45:24 +0100 Subject: [PATCH 29/85] Introduce typed body for V2 native-auth HAL follow-up requests Replace the hand-assembled [AnyHashable: Any] body dictionaries in the V2 HAL request provider with a typed MSALNativeAuthV2RequestBody. Each field maps to a MSALNativeAuthV2RequestBodyKey and is emitted only when set, so call sites state just the fields an operation carries (e.g. a password submit sets `password`, a poll sets nothing extra). The generated body dictionary is unchanged, so the wire format is preserved. - Add MSALNativeAuthV2RequestBody (typed continuationToken + optional fields) - MSALNativeAuthV2HrefParameters now carries the typed body and derives `body` - Route the 8 HAL provider methods through the typed body - Register the new file in project.pbxproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- MSAL/MSAL.xcodeproj/project.pbxproj | 6 ++ .../v2/MSALNativeAuthV2RequestBody.swift | 64 +++++++++++++++++++ .../v2/MSALNativeAuthV2RequestProvider.swift | 35 +++------- .../MSALNativeAuthV2HrefParameters.swift | 8 ++- 4 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index e52c94664f..6e432b78d3 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; + 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; 1E1A2E042256D12F001009ED /* MSALTestAppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64B01E5AAC5C0086D120 /* MSALTestAppSettings.m */; }; @@ -425,6 +426,7 @@ 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; + 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; @@ -2317,6 +2319,7 @@ 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2LinkRelation.swift; sourceTree = ""; }; 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBodyKey.swift; sourceTree = ""; }; + 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBody.swift; sourceTree = ""; }; 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthRegistrationRequiredState.swift; sourceTree = ""; }; 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFAVerificationRequiredState.swift; sourceTree = ""; }; 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthNewPasswordRequiredState.swift; sourceTree = ""; }; @@ -3208,6 +3211,7 @@ 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */, 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */, 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */, + 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */, F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */, B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */, D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */, @@ -7483,6 +7487,7 @@ 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */, F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */, D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, + 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */, 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, @@ -7817,6 +7822,7 @@ 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */, 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */, 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, + 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */, 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift new file mode 100644 index 0000000000..a99ddeec92 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift @@ -0,0 +1,64 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Typed body of a Native Auth V2 (HAL) follow-up request. Each field maps to a +/// ``MSALNativeAuthV2RequestBodyKey`` and is emitted only when set, so a caller states just the +/// fields the operation carries (e.g. a password submit sets `password`, a poll sets nothing extra). +/// Owning the key mapping here keeps the request provider free of raw body dictionaries. +struct MSALNativeAuthV2RequestBody { + let continuationToken: String + var password: String? + var code: String? + var otp: String? + var newPassword: String? + var target: String? + var attributes: [String: Any]? + + var dictionary: [String: Any] { + var body: [String: Any] = [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] + + if let password = password { + body[MSALNativeAuthV2RequestBodyKey.password.rawValue] = password + } + if let code = code { + body[MSALNativeAuthV2RequestBodyKey.code.rawValue] = code + } + if let otp = otp { + body[MSALNativeAuthV2RequestBodyKey.otp.rawValue] = otp + } + if let newPassword = newPassword { + body[MSALNativeAuthV2RequestBodyKey.newPassword.rawValue] = newPassword + } + if let target = target { + body[MSALNativeAuthV2RequestBodyKey.target.rawValue] = target + } + if let attributes = attributes { + body[MSALNativeAuthV2RequestBodyKey.attributes.rawValue] = attributes + } + + return body + } +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index dcd499c970..d670574b85 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -184,10 +184,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.submitPassword.rawValue, - body: [ - MSALNativeAuthV2RequestBodyKey.password.rawValue: password, - MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken - ] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, password: password) )) } @@ -202,10 +199,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.submitCode.rawValue, - body: [ - MSALNativeAuthV2RequestBodyKey.code.rawValue: code, - MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken - ] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, code: code) )) } @@ -220,10 +214,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, - body: [ - MSALNativeAuthV2RequestBodyKey.attributes.rawValue: attributes, - MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken - ] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, attributes: attributes) )) } @@ -232,17 +223,13 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { continuationToken: String, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { - var body: [AnyHashable: Any] = [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] - if let target = target { - body[MSALNativeAuthV2RequestBodyKey.target.rawValue] = target - } return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.registerMethod.rawValue, - body: body + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, target: target) )) } @@ -256,7 +243,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.challenge.rawValue, - body: [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken) )) } @@ -271,10 +258,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.verify.rawValue, - body: [ - MSALNativeAuthV2RequestBodyKey.otp.rawValue: otp, - MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken - ] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, otp: otp) )) } @@ -289,10 +273,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "PUT", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, - body: [ - MSALNativeAuthV2RequestBodyKey.newPassword.rawValue: newPassword, - MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken - ] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, newPassword: newPassword) )) } @@ -306,7 +287,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: .telemetryApiIdV2Hal, operationType: MSALNativeAuthV2OperationType.poll.rawValue, - body: [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] + requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken) )) } diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift index 009002eeec..27595f3fa0 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift @@ -25,16 +25,20 @@ import Foundation /// A HAL follow-up request driven by a server-provided `href` (challenge, verify, submit*, register, -/// update-password, poll). JSON encoded with a caller-supplied body. +/// update-password, poll). JSON encoded with a typed ``MSALNativeAuthV2RequestBody``. struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { let context: MSALNativeAuthRequestContext let href: String let httpMethod: String let apiId: MSALNativeAuthTelemetryApiId let operationType: MSALNativeAuthOperationType - let body: [AnyHashable: Any] + let requestBody: MSALNativeAuthV2RequestBody let encoding: MSALNativeAuthUrlRequestEncoding = .json + var body: [AnyHashable: Any] { + return requestBody.dictionary + } + func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { return try resolver.url(forHref: href) } From c6160840ebba22a39dd4d7eaa872759994a69183 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 16:59:01 +0100 Subject: [PATCH 30/85] Add context-threaded, PII-masked logging to V2 native-auth validator (V1-style) Thread MSIDRequestContext through the V2 response validator's three validate methods (mirroring V1 validators) and emit decision/error log statements. Server error messages and transport failures are masked via MSALLogMask.maskPII; no tokens, continuation tokens, or hints are logged. Updates the flow controller call sites, the validator mock, and tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../v2/MSALNativeAuthFlowController.swift | 6 +- .../MSALNativeAuthV2ResponseValidator.swift | 97 +++++++++++++++---- ...SALNativeAuthV2ResponseValidatorMock.swift | 12 ++- ...ALNativeAuthV2ResponseValidatorTests.swift | 43 ++++---- 4 files changed, 114 insertions(+), 44 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index c87d1127c6..e78068b727 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -638,7 +638,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let result: Result = await send { try self.requestProvider.authorizeChallengeStart(context: context) } - return responseValidator.validateAuthorizeChallenge(result, flowScenario: flowScenario) + return responseValidator.validateAuthorizeChallenge(context: context, result, flowScenario: flowScenario) } private func performAuthorizeChallengeContinue( @@ -649,7 +649,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let result: Result = await send { try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, context: context) } - return responseValidator.validateAuthorizeChallenge(result, flowScenario: flowScenario) + return responseValidator.validateAuthorizeChallenge(context: context, result, flowScenario: flowScenario) } private func performInteraction( @@ -657,7 +657,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati requestBuilder: @escaping () throws -> MSIDHttpRequest ) async -> MSALNativeAuthV2InteractionValidatedResponse { let result: Result = await send(requestBuilder) - return responseValidator.validateInteraction(result) + return responseValidator.validateInteraction(context: context, result) } private func send( diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index c2cd3662b2..03e41b1236 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -22,44 +22,56 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation +@_implementationOnly import MSAL_Private /// Maps a raw ``MSALNativeAuthHALResponse`` (or transport error) into a validated, controller-facing response. protocol MSALNativeAuthV2ResponseValidating { func validateAuthorizeChallenge( + context: MSIDRequestContext, _ result: Result, flowScenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse - func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse - func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse + func validateInteraction( + context: MSIDRequestContext, + _ result: Result + ) -> MSALNativeAuthV2InteractionValidatedResponse + func validateToken( + context: MSIDRequestContext, + _ result: Result + ) -> MSALNativeAuthV2TokenValidatedResponse } final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidating { func validateAuthorizeChallenge( + context: MSIDRequestContext, _ result: Result, flowScenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { switch result { case .failure(let error): - return .error(flowError(from: error)) + return .error(flowError(from: error, context: context)) case .success(let response): if let error = response.error { - return .error(flowError(from: error)) + return .error(flowError(from: error, context: context)) } if let code = response.code { + MSALNativeAuthLogger.log(level: .verbose, context: context, format: "authorize-challenge: received authorization code") return .authorizationCode(code: code) } if let continuationToken = response.continuationToken { let relation = flowScenario.link guard let href = response.links[relation] else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "authorize-challenge: missing '%@' link", relation) return .error(MSALNativeAuthFlowError( type: .generalError, errorDescription: "Invalid authorize-challenge response: missing '\(relation)' link" )) } + MSALNativeAuthLogger.log(level: .verbose, context: context, format: "authorize-challenge: received continuation token") return .continuationToken(continuationToken: continuationToken, href: href) } + MSALNativeAuthLogger.log(level: .error, context: context, format: "authorize-challenge: neither a continuation token nor a code") return .error(MSALNativeAuthFlowError( type: .generalError, errorDescription: "authorize-challenge returned neither a continuation token nor a code" @@ -69,37 +81,51 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin // swiftlint:disable:next cyclomatic_complexity function_body_length func validateInteraction( + context: MSIDRequestContext, _ result: Result ) -> MSALNativeAuthV2InteractionValidatedResponse { switch result { case .failure(let error): - return .error(flowError(from: error)) + return .error(flowError(from: error, context: context)) case .success(let response): if let error = response.error { - return .error(flowError(from: error)) + return .error(flowError(from: error, context: context)) } if response.isReadyToComplete { guard let continuationToken = response.continuationToken else { + MSALNativeAuthLogger.log( + level: .error, + context: context, + format: "interaction: missing continuation token in 'continue' response") return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in 'continue' response")) } + MSALNativeAuthLogger.log(level: .info, context: context, format: "interaction: flow ready to complete") return .readyToComplete(continuationToken: continuationToken) } guard let continuationToken = response.continuationToken else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: missing continuation token in interaction response") return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in interaction response")) } // Sign-in method discovery: no action, but the available methods are embedded. if response.action == nil, !response.methods.isEmpty { + MSALNativeAuthLogger.log( + level: .verbose, + context: context, + format: "interaction: returning %d sign-in methods", + response.methods.count) return .signInMethods(continuationToken: continuationToken, methods: response.methods) } + MSALNativeAuthLogger.log(level: .verbose, context: context, format: "interaction: processing action '%@'", response.action ?? "nil") + switch response.halAction { case .challenge: let method = response.methods.first guard let challengeHref = method?.link(for: .challenge) ?? response.href(for: .challenge) else { - return missingLink(.challenge) + return missingLink(.challenge, context: context) } // MFA required: the server sets challengeContext.authenticationFactor to "multiFactor" // and embeds the available second-factor methods. Surface them for method selection @@ -126,7 +152,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) } guard let verifyHref = response.href(for: .verify) else { - return missingLink(.verify) + return missingLink(.verify, context: context) } // An email/OOB method carries a hint and/or a code length; a password method does not. if (response.codeLength ?? 0) > 0 || response.hint != nil || response.methodType == "email" { @@ -141,7 +167,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .passwordRequired(continuationToken: continuationToken, verifyHref: verifyHref) case .enroll, .register: guard let enrollHref = response.href(for: .enroll) ?? response.href(for: .register) else { - return missingLink(.enroll) + return missingLink(.enroll, context: context) } return .registrationRequired( continuationToken: continuationToken, @@ -150,7 +176,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case .activate: guard let activateHref = response.href(for: .activate) else { - return missingLink(.activate) + return missingLink(.activate, context: context) } return .activationRequired( continuationToken: continuationToken, @@ -160,7 +186,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case .collectAttributes: guard let submitHref = response.href(for: .submitAttributes) else { - return missingLink(.submitAttributes) + return missingLink(.submitAttributes, context: context) } return .attributesRequired( continuationToken: continuationToken, @@ -169,7 +195,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case .update: guard let updateHref = response.href(for: .update) ?? response.href(for: .self) else { - return missingLink(.update) + return missingLink(.update, context: context) } return .updateRequired( continuationToken: continuationToken, @@ -177,28 +203,31 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) case .poll: guard let pollHref = response.href(for: .poll) else { - return missingLink(.poll) + return missingLink(.poll, context: context) } return .pollInProgress( continuationToken: continuationToken, pollHref: pollHref ) default: + MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: unexpected action '%@'", response.action ?? "nil") return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) } } } func validateToken( + context: MSIDRequestContext, _ result: Result ) -> MSALNativeAuthV2TokenValidatedResponse { switch result { case .failure(let error): - return .error(flowError(from: error)) + return .error(flowError(from: error, context: context)) case .success(let response): if let error = response.error { - return .error(flowError(from: error)) + return .error(flowError(from: error, context: context)) } + MSALNativeAuthLogger.log(level: .verbose, context: context, format: "token: exchange succeeded") return .success(accessToken: response.accessToken) } } @@ -207,14 +236,18 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin /// The server returned an action that requires a follow-up link, but that link is absent. /// Fail here rather than passing a missing href down to the next request. - private func missingLink(_ relation: MSALNativeAuthV2LinkRelation) -> MSALNativeAuthV2InteractionValidatedResponse { + private func missingLink( + _ relation: MSALNativeAuthV2LinkRelation, + context: MSIDRequestContext + ) -> MSALNativeAuthV2InteractionValidatedResponse { + MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: missing '%@' link", relation.rawValue) return .error(MSALNativeAuthFlowError( type: .generalError, errorDescription: "Invalid interaction response: missing '\(relation.rawValue)' link" )) } - private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError) -> MSALNativeAuthFlowError { + private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError, context: MSIDRequestContext) -> MSALNativeAuthFlowError { let message = serverError.message let errorCodes = estsErrorCodes(from: message) let type: MSALNativeAuthFlowError.ErrorType @@ -236,6 +269,8 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin type = .generalError } + logServerError(serverError, type: type, context: context) + return MSALNativeAuthFlowError( type: type, errorDescription: message, @@ -244,6 +279,25 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin ) } + private func logServerError( + _ serverError: MSALNativeAuthHALResponse.ServerError, + type: MSALNativeAuthFlowError.ErrorType, + context: MSIDRequestContext + ) { + MSALNativeAuthLogger.log( + level: .error, + context: context, + format: "server error mapped to '%@' (code: %@, innerErrorCode: %@)", + String(describing: type), + serverError.code ?? "nil", + serverError.innerErrorCode ?? "nil") + MSALNativeAuthLogger.logPII( + level: .error, + context: context, + format: "server error message: %@", + MSALLogMask.maskPII(serverError.message)) + } + /// Extracts the numeric ESTS error codes (e.g. `50126` from `AADSTS50126`) embedded in a /// server error message. private func estsErrorCodes(from message: String?) -> [Int] { @@ -267,10 +321,15 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return codes } - private func flowError(from error: Error) -> MSALNativeAuthFlowError { + private func flowError(from error: Error, context: MSIDRequestContext) -> MSALNativeAuthFlowError { if let flowError = error as? MSALNativeAuthFlowError { return flowError } + MSALNativeAuthLogger.logPII( + level: .error, + context: context, + format: "transport failure: %@", + MSALLogMask.maskPII((error as NSError).localizedDescription)) return MSALNativeAuthFlowError( type: .generalError, errorDescription: (error as NSError).localizedDescription diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift index 7cb8ee08da..9a2b0ee003 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift @@ -24,6 +24,7 @@ import Foundation @testable import MSAL +@_implementationOnly import MSAL_Private class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating { @@ -36,6 +37,7 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating private(set) var validateTokenCallCount = 0 func validateAuthorizeChallenge( + context: MSIDRequestContext, _ result: Result, flowScenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { @@ -46,7 +48,10 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating return .error(MSALNativeAuthFlowError(type: .generalError)) } - func validateInteraction(_ result: Result) -> MSALNativeAuthV2InteractionValidatedResponse { + func validateInteraction( + context: MSIDRequestContext, + _ result: Result + ) -> MSALNativeAuthV2InteractionValidatedResponse { defer { validateInteractionCallCount += 1 } if validateInteractionCallCount < interactionResponses.count { return interactionResponses[validateInteractionCallCount] @@ -54,7 +59,10 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating return .error(MSALNativeAuthFlowError(type: .generalError)) } - func validateToken(_ result: Result) -> MSALNativeAuthV2TokenValidatedResponse { + func validateToken( + context: MSIDRequestContext, + _ result: Result + ) -> MSALNativeAuthV2TokenValidatedResponse { validateTokenCallCount += 1 return tokenResponse } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 8e2e63eda9..56d4fb5b09 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -24,14 +24,17 @@ import XCTest @testable import MSAL +@_implementationOnly import MSAL_Private final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { private var sut: MSALNativeAuthV2ResponseValidator! + private var context: MSALNativeAuthRequestContext! override func setUp() { super.setUp() sut = MSALNativeAuthV2ResponseValidator() + context = MSALNativeAuthRequestContextMock() } // MARK: - Builders @@ -77,13 +80,13 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateAuthorizeChallenge_withContinuationToken() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .passwordReset) + let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .passwordReset) XCTAssertEqual(result, .continuationToken(continuationToken: "ct", href: "https://contoso.com/reset")) } func test_validateAuthorizeChallenge_missingFlowLink_returnsError() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .signUp) + let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .signUp) XCTAssertEqual(result, .error(MSALNativeAuthFlowError( type: .generalError, errorDescription: "Invalid authorize-challenge response: missing 'sign_up' link" @@ -92,19 +95,19 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateAuthorizeChallenge_withAuthorizationCode() { let response = makeResponse(code: "auth-code") - let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .signIn) + let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) XCTAssertEqual(result, .authorizationCode(code: "auth-code")) } func test_validateAuthorizeChallenge_withServerError_returnsError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateAuthorizeChallenge(.success(response), flowScenario: .signIn) + let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateAuthorizeChallenge_withTransportFailure_returnsError() { - let result = sut.validateAuthorizeChallenge(.failure(ErrorMock.error), flowScenario: .signIn) + let result = sut.validateAuthorizeChallenge(context: context, .failure(ErrorMock.error), flowScenario: .signIn) guard case .error = result else { return XCTFail("Expected error") } @@ -115,7 +118,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateInteraction_challengeAction_returnsChallengeRequired() { let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) let response = makeResponse(state: "interactionRequired", action: "challenge", continuationToken: "ct", methods: [method]) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .challengeRequired(continuationToken: "ct", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com")) } @@ -128,7 +131,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { authenticationFactor: "multiFactor", methods: [method] ) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .mfaRequired(continuationToken: "ct", methods: [method], challengeHref: "https://contoso.com/challenge")) } @@ -141,70 +144,70 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { hint: "u***@contoso.com", links: ["verify": "https://contoso.com/verify", "resend": "https://contoso.com/resend"] ) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8)) } func test_validateInteraction_updateAction_returnsUpdateRequired() { let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct", links: ["update": "https://contoso.com/update"]) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .updateRequired(continuationToken: "ct", updateHref: "https://contoso.com/update")) } func test_validateInteraction_pollAction_returnsPollInProgress() { let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct", links: ["poll": "https://contoso.com/poll"]) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .pollInProgress(continuationToken: "ct", pollHref: "https://contoso.com/poll")) } func test_validateInteraction_updateAction_withoutUpdateLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct") - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_pollAction_withoutPollLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct") - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_verifyAction_withoutVerifyLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "verify", continuationToken: "ct", codeLength: 8, hint: "u***@contoso.com") - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_collectAttributesAction_withoutSubmitLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "collectAttributes", continuationToken: "ct") - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } func test_validateInteraction_continueState_returnsReadyToComplete() { let response = makeResponse(state: "continue", continuationToken: "ct") - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .readyToComplete(continuationToken: "ct")) } func test_validateInteraction_userNotFound_mapsToUserNotFound() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "AADSTS50034 user not found", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .userNotFound))) } func test_validateInteraction_invalidGrant_mapsToInvalidCode() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "wrong code", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCode))) } func test_validateInteraction_invalidContinuationToken_mapsCorrectly() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad token", innerErrorCode: "invalidContinuationToken", correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(.success(response)) + let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidContinuationToken))) } @@ -212,7 +215,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateToken_success() { let response = makeResponse(accessToken: "access-token") - let result = sut.validateToken(.success(response)) + let result = sut.validateToken(context: context, .success(response)) guard case .success(let accessToken) = result else { return XCTFail("Expected success") } @@ -222,7 +225,7 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { func test_validateToken_withServerError_returnsError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "bad", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateToken(.success(response)) + let result = sut.validateToken(context: context, .success(response)) guard case .error = result else { return XCTFail("Expected error") } From 574e3e02acd34f49eabeea2a158f0b1f5e8ea035 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:06:57 +0100 Subject: [PATCH 31/85] Add version control rule: never commit or push without explicit user request Codify in AGENTS.md that AI agents must not run git commit/push (or any history/remote-mutating git command) unless the user explicitly asks, so changes are always reviewed first. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .clinerules/AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.clinerules/AGENTS.md b/.clinerules/AGENTS.md index 1e0a39c8a2..c70a7bcc11 100644 --- a/.clinerules/AGENTS.md +++ b/.clinerules/AGENTS.md @@ -30,6 +30,10 @@ When creating a new application with MSAL authentication, users need to select a AI agents MUST build and run tests using the build/test configuration already set up in Xcode — i.e. the schemes defined in `MSAL.xcworkspace`, driven through `build.py` (e.g. `./build.py --targets iosFramework macFramework`). Always use `MSAL.xcworkspace`, never open or build `MSAL.xcodeproj` directly, and do not invent ad-hoc `xcodebuild` invocations, schemes, or configurations that diverge from the ones configured in the workspace. If a build/test run needs a specific simulator, select an available one via the `IOS_SIM_DEVICE` / `IOS_SIM_OS` environment variables (consumed by `build.py`) rather than changing the scheme or configuration. +## Version control guidelines + +AI agents MUST NOT run `git commit`, `git push`, or any other history- or remote-mutating git command unless the user has explicitly asked for it in the current request. Make and stage changes, then stop and let the user review; wait for an explicit instruction before committing or pushing. Never commit or push proactively "to be helpful" — the user always reviews changes first. + ## MSAL API usage Sample code snippets for both Swift & Objective-C can be found in the file `.clinerules/03-MSAL-API-usage.md` From 78ac89ad1f5dd23c45edf02c386e411e8342ef93 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:33:03 +0100 Subject: [PATCH 32/85] Fix V2 FlowController completion tests and resolve SwiftLint issues Make the V2 request-provider mock's token() return a valid token JSON dict and set expectedMSIDTokenResult in the 8 completion tests so the real token-exchange/cache path succeeds. Wrap long lines in the flow controller and split the validator's error-mapping helpers into an extension to satisfy SwiftLint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../v2/MSALNativeAuthFlowController.swift | 22 +++++++++++++++---- .../MSALNativeAuthV2ResponseValidator.swift | 3 +++ .../MSALNativeAuthFlowControllerTests.swift | 9 ++++++++ .../MSALNativeAuthV2RequestProviderMock.swift | 20 +++++++++++++++-- 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index e78068b727..02b944da1e 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -301,10 +301,18 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati scopes: continuation.scopes ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context, scenario: continuation.flowScenario) + return response( + .actionRequired(action: .newPasswordRequired, newState: newState), + context: context, + scenario: continuation.flowScenario) case .error(let error): // Recoverable: allow the app to retry with the same code-required state. - return interactionFailure(result, event: event, context: context, scenario: continuation.flowScenario, newState: error.isInvalidCode ? state : nil) + return interactionFailure( + result, + event: event, + context: context, + scenario: continuation.flowScenario, + newState: error.isInvalidCode ? state : nil) default: return interactionFailure(result, event: event, context: context, scenario: continuation.flowScenario, newState: nil) } @@ -838,7 +846,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), context: context, scenario: flowScenario) + return response( + .actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), + context: context, + scenario: flowScenario) case .registrationRequired(let token, let enrollHref, let methods): let (authMethods, methodLinks) = authMethods(from: methods) let newState = makeState( @@ -853,7 +864,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), context: context, scenario: flowScenario) + return response( + .actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), + context: context, + scenario: flowScenario) case .activationRequired(let token, let activateHref, let sentTo, let codeLength): let newState = makeState( flowScenario, diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 03e41b1236..4000bfd787 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -231,6 +231,9 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .success(accessToken: response.accessToken) } } +} + +extension MSALNativeAuthV2ResponseValidator { // MARK: - Error mapping diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 5c8f387cc8..e539e31327 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -26,6 +26,7 @@ import XCTest @testable import MSAL @_implementationOnly import MSAL_Private +// swiftlint:disable type_body_length file_length final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { private var sut: MSALNativeAuthFlowController! @@ -178,6 +179,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/verify")!]) let response = await sut.submitCode("12345678", state: state) @@ -195,6 +197,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signUp, links: ["verify": URL(string: "https://contoso.com/verify")!]) let response = await sut.submitCode("12345678", state: state) @@ -219,6 +222,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .authorizationCode(code: "auth-code") ] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(links: ["update": URL(string: "https://contoso.com/update")!]) let response = await sut.submitNewPassword("New-Password-1", state: state) @@ -315,6 +319,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .readyToComplete(continuationToken: "ct-continue") ] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let parameters = MSALNativeAuthSignUpParametersV2(username: "user@contoso.com") parameters.password = "Secret-Password-1" @@ -384,6 +389,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .readyToComplete(continuationToken: "ct-4") ] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let params = MSALNativeAuthSignInParameters(username: "user@contoso.com") params.password = "password" @@ -437,6 +443,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signUp, links: ["submitAttributes": URL(string: "https://contoso.com/submitattributes")!]) let response = await sut.submitAttributes(["displayName": "User"], state: state) @@ -452,6 +459,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/mfa/verify")!]) let response = await sut.submitChallenge("12345678", state: state) @@ -470,6 +478,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] validatorMock.tokenResponse = .success(accessToken: "access-token") + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signIn, links: ["activate": URL(string: "https://contoso.com/jit/activate")!]) let response = await sut.submitChallenge("12345678", state: state) diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index 354802534d..97375c5550 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -83,8 +83,24 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { tokenCalled = true tokenScopes = scopes - return try resolveRequest() - } + if throwError { + throw ErrorMock.error + } + // The token endpoint response is parsed for real (it is not routed through the validator mock), + // so stub a valid token payload rather than the empty default used by the HAL endpoints. + let request = MSIDHttpRequest() + HttpModuleMockConfigurator.configure(request: request, responseJson: MSALNativeAuthV2RequestProviderMock.successfulTokenResponseJson) + return request + } + + static let successfulTokenResponseJson: [String: Any] = [ + "token_type": "Bearer", + "access_token": "access-token", + "id_token": "idToken", + "refresh_token": "refresh-token", + "expires_in": 3600, + "scope": "scope" + ] func resetPasswordStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { resetPasswordStartCalled = true From f4c100f69d27fe6834912ce9cdd16de54e39c533 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:37:44 +0100 Subject: [PATCH 33/85] merged dev into branch --- MSAL/IdentityCore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/IdentityCore b/MSAL/IdentityCore index 54f79c70b4..16c26a474f 160000 --- a/MSAL/IdentityCore +++ b/MSAL/IdentityCore @@ -1 +1 @@ -Subproject commit 54f79c70b42bc04d86fd0f18e8cf9d40d36d3a94 +Subproject commit 16c26a474ff5e98dbdc618caf834b187d56e2370 From 608c98af98130e91fd86d84f39ccc9f9f65c3e90 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:38:25 +0100 Subject: [PATCH 34/85] Revert "merged dev into branch" This reverts commit f4c100f69d27fe6834912ce9cdd16de54e39c533. --- MSAL/IdentityCore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/IdentityCore b/MSAL/IdentityCore index 16c26a474f..54f79c70b4 160000 --- a/MSAL/IdentityCore +++ b/MSAL/IdentityCore @@ -1 +1 @@ -Subproject commit 16c26a474ff5e98dbdc618caf834b187d56e2370 +Subproject commit 54f79c70b42bc04d86fd0f18e8cf9d40d36d3a94 From 8753280ba46067bcda9a36813f21a1f1282b10a4 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:38:39 +0100 Subject: [PATCH 35/85] Correct IC --- MSAL/IdentityCore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/IdentityCore b/MSAL/IdentityCore index 54f79c70b4..16c26a474f 160000 --- a/MSAL/IdentityCore +++ b/MSAL/IdentityCore @@ -1 +1 @@ -Subproject commit 54f79c70b42bc04d86fd0f18e8cf9d40d36d3a94 +Subproject commit 16c26a474ff5e98dbdc618caf834b187d56e2370 From 32094fc2a7e311b3fd712e200cd06841abe57a57 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:39:23 +0100 Subject: [PATCH 36/85] Revert "Correct IC" This reverts commit 8753280ba46067bcda9a36813f21a1f1282b10a4. --- MSAL/IdentityCore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/IdentityCore b/MSAL/IdentityCore index 16c26a474f..54f79c70b4 160000 --- a/MSAL/IdentityCore +++ b/MSAL/IdentityCore @@ -1 +1 @@ -Subproject commit 16c26a474ff5e98dbdc618caf834b187d56e2370 +Subproject commit 54f79c70b42bc04d86fd0f18e8cf9d40d36d3a94 From bf2b80e374dff4143473e1e2d0f6a350e64c0ea0 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:47:16 +0100 Subject: [PATCH 37/85] removed not needed cache accessor --- .../controllers/v2/MSALNativeAuthFlowController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 02b944da1e..212ae3525c 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -33,7 +33,6 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private let config: MSALNativeAuthInternalConfiguration private let requestProvider: MSALNativeAuthV2RequestProviding private let responseValidator: MSALNativeAuthV2ResponseValidating - private let cacheAccessor: MSALNativeAuthCacheInterface private let resultFactory: MSALNativeAuthResultBuildable private let tokenCacher: MSALNativeAuthTokenCacher @@ -50,7 +49,6 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati self.config = config self.requestProvider = requestProvider self.responseValidator = responseValidator - self.cacheAccessor = cacheAccessor self.resultFactory = resultFactory self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) super.init(clientId: config.clientId) From 0d127ad5d677708e99974c92f02130ee3c8fb607 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 21 Jul 2026 17:50:06 +0100 Subject: [PATCH 38/85] not needed comment --- .../controllers/v2/MSALNativeAuthFlowContinuationState.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift index 8ec648656a..bc6c4ebf0d 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -38,7 +38,6 @@ struct MSALNativeAuthFlowContinuationState { let username: String? let sentToHint: String? let codeLength: Int? - /// Auth methods offered for MFA / strong-auth (JIT) selection. let authMethods: [MSALAuthMethod] /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final /// `/token` exchange. Threaded through every step. From 3f2100643fcbc8bc5680281619734b1f211d964a Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 09:41:49 +0100 Subject: [PATCH 39/85] Comments --- .../controllers/v2/MSALNativeAuthFlowContinuationState.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift index bc6c4ebf0d..87ebe2de57 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -34,6 +34,8 @@ struct MSALNativeAuthFlowContinuationState { /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", /// "challenge", "enroll", "activate", "submitAttributes"). let links: [String: URL] + /// Resolved per-auth-method action links keyed by auth-method id (MFA `challenge` / JIT `enroll`). + /// Separate from ``links`` because it uses a different key space (method ids, not relations). let methodLinks: [String: URL] let username: String? let sentToHint: String? From ab433a156d98b51db5e8f5cfa9b3a624dabdf062 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 09:55:34 +0100 Subject: [PATCH 40/85] Unify V2 continuation-state links behind a typed LinkKey Replace the separate links/methodLinks maps on MSALNativeAuthFlowContinuationState with a single [MSALNativeAuthV2LinkKey: URL] map, where the key is either a flow relation or a per-auth-method link. This removes the dead cacheAccessor stored property from the flow controller and keeps link lookups type-safe without a stringly-typed key namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../MSALNativeAuthFlowContinuationState.swift | 22 +++++++++---------- .../v2/MSALNativeAuthFlowController.swift | 8 +++---- .../MSALNativeAuthFlowControllerTests.swift | 14 ++++++++++-- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift index 87ebe2de57..badc5609d0 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -24,6 +24,13 @@ import Foundation +/// Typed key for a continuation state's link map: either a flow `_links` relation or a +/// per-auth-method action link (keyed by method id). +enum MSALNativeAuthV2LinkKey: Hashable { + case relation(MSALNativeAuthV2LinkRelation) + case method(id: String) +} + /// Internal continuation context carried by a ``MSALNativeAuthFlowInternalState``. /// /// Holds the opaque server `continuation_token` and the resolved `_links` hrefs @@ -31,12 +38,7 @@ import Foundation struct MSALNativeAuthFlowContinuationState { let flowScenario: MSALNativeAuthFlowScenario let continuationToken: String - /// Resolved `_links` keyed by relation (e.g. "verify", "resend", "update", "poll", "continue", - /// "challenge", "enroll", "activate", "submitAttributes"). - let links: [String: URL] - /// Resolved per-auth-method action links keyed by auth-method id (MFA `challenge` / JIT `enroll`). - /// Separate from ``links`` because it uses a different key space (method ids, not relations). - let methodLinks: [String: URL] + let links: [MSALNativeAuthV2LinkKey: URL] let username: String? let sentToHint: String? let codeLength: Int? @@ -58,8 +60,7 @@ struct MSALNativeAuthFlowContinuationState { init( flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, - links: [String: URL], - methodLinks: [String: URL] = [:], + links: [MSALNativeAuthV2LinkKey: URL], username: String?, sentToHint: String? = nil, codeLength: Int? = nil, @@ -71,7 +72,6 @@ struct MSALNativeAuthFlowContinuationState { self.flowScenario = flowScenario self.continuationToken = continuationToken self.links = links - self.methodLinks = methodLinks self.username = username self.sentToHint = sentToHint self.codeLength = codeLength @@ -82,11 +82,11 @@ struct MSALNativeAuthFlowContinuationState { } func link(_ relation: MSALNativeAuthV2LinkRelation) -> URL? { - return links[relation.rawValue] + return links[.relation(relation)] } /// The challenge / enroll link associated with a specific auth method. func methodLink(for methodId: String) -> URL? { - return methodLinks[methodId] + return links[.method(id: methodId)] } } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 212ae3525c..38f1a80b74 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -1012,23 +1012,21 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: Set = [] ) -> MSALNativeAuthFlowInternalState { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) - var resolvedLinks: [String: URL] = [:] + var resolvedLinks: [MSALNativeAuthV2LinkKey: URL] = [:] for (relation, href) in links { if let href = href, let url = try? resolver.url(forHref: href) { - resolvedLinks[relation.rawValue] = url + resolvedLinks[.relation(relation)] = url } } - var resolvedMethodLinks: [String: URL] = [:] for (methodId, href) in methodLinks { if let url = try? resolver.url(forHref: href) { - resolvedMethodLinks[methodId] = url + resolvedLinks[.method(id: methodId)] = url } } let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, continuationToken: continuationToken, links: resolvedLinks, - methodLinks: resolvedMethodLinks, username: username, sentToHint: sentToHint, codeLength: codeLength, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index e539e31327..c6fcefdeab 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -58,7 +58,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { let continuation = MSALNativeAuthFlowContinuationState( flowScenario: .passwordReset, continuationToken: continuationToken, - links: links, + links: relationLinks(links), username: "user@contoso.com", sentToHint: "u***@contoso.com", codeLength: 8 @@ -66,6 +66,16 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) } + private func relationLinks(_ links: [String: URL]) -> [MSALNativeAuthV2LinkKey: URL] { + var typed: [MSALNativeAuthV2LinkKey: URL] = [:] + for (rawRelation, url) in links { + if let relation = MSALNativeAuthV2LinkRelation(rawValue: rawRelation) { + typed[.relation(relation)] = url + } + } + return typed + } + private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParametersV2 { let params = MSALNativeAuthResetPasswordParametersV2(username: "user@contoso.com") return params @@ -278,7 +288,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, continuationToken: continuationToken, - links: links, + links: relationLinks(links), username: "user@contoso.com", sentToHint: "u***@contoso.com", codeLength: 8, From a74b7ac74c0abfd87b08631102f065db1ff15199 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 10:30:44 +0100 Subject: [PATCH 41/85] Remove KVC body hack from V2 request configurator Inject the V2 HAL request body into MSALNativeAuthUrlRequestSerializer via a new optional body init parameter instead of smuggling the nested dictionary into MSIDHttpRequest.parameters with setValue(forKey:). The serializer serializes body when present, otherwise the parameters argument, so V1 is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../MSALNativeAuthUrlRequestSerializer.swift | 15 +++++++++++---- .../v2/MSALNativeAuthV2RequestConfigurator.swift | 7 ++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/MSAL/src/native_auth/network/MSALNativeAuthUrlRequestSerializer.swift b/MSAL/src/native_auth/network/MSALNativeAuthUrlRequestSerializer.swift index ca3119cac6..9145926be0 100644 --- a/MSAL/src/native_auth/network/MSALNativeAuthUrlRequestSerializer.swift +++ b/MSAL/src/native_auth/network/MSALNativeAuthUrlRequestSerializer.swift @@ -33,10 +33,16 @@ final class MSALNativeAuthUrlRequestSerializer: NSObject, MSIDRequestSerializati private let context: MSIDRequestContext private let encoding: MSALNativeAuthUrlRequestEncoding + private let body: [AnyHashable: Any]? - init(context: MSIDRequestContext, encoding: MSALNativeAuthUrlRequestEncoding) { + /// When non-nil, `body` is serialized as the HTTP body instead of the `parameters` argument passed + /// to `serialize(with:parameters:headers:)`. This lets callers supply a nested JSON body that does + /// not fit `MSIDHttpRequest.parameters` (`[String: String]`). When nil, the `parameters` argument + /// is used, preserving the default behavior. + init(context: MSIDRequestContext, encoding: MSALNativeAuthUrlRequestEncoding, body: [AnyHashable: Any]? = nil) { self.context = context self.encoding = encoding + self.body = body } func serialize( @@ -47,6 +53,7 @@ final class MSALNativeAuthUrlRequestSerializer: NSObject, MSIDRequestSerializati var request = request var requestHeaders: [String: String] = [:] + let body = self.body ?? parameters // Convert entries from `headers` to a dictionary [String: String] @@ -59,9 +66,9 @@ final class MSALNativeAuthUrlRequestSerializer: NSObject, MSIDRequestSerializati } if encoding == .json { - if JSONSerialization.isValidJSONObject(parameters) { + if JSONSerialization.isValidJSONObject(body) { do { - let jsonData = try JSONSerialization.data(withJSONObject: parameters) + let jsonData = try JSONSerialization.data(withJSONObject: body) request.httpBody = jsonData } catch { MSALNativeAuthLogger.log( @@ -74,7 +81,7 @@ final class MSALNativeAuthUrlRequestSerializer: NSObject, MSIDRequestSerializati MSALNativeAuthLogger.log(level: .error, context: context, format: "HTTP body request serialization failed") } } else { - let encodedBody = formUrlEncode(parameters) + let encodedBody = formUrlEncode(body) request.httpBody = encodedBody.data(using: .utf8) } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift index 8dcebd7b31..32da6669df 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -50,10 +50,6 @@ final class MSALNativeAuthV2RequestConfigurator: MSIDAADRequestConfigurator { let rawJSONResponseSerializer = request.responseSerializer request.context = parameters.context - // `MSIDHttpRequest.parameters` is typed `[String: String]` for AAD form posts, but V2 HAL bodies - // can contain nested JSON (e.g. the sign-up `attributes` object). Assign via KVC so the nested - // dictionary is preserved and JSON-serialized as-is by `MSALNativeAuthUrlRequestSerializer`. - request.setValue(parameters.body, forKey: "parameters") var urlRequest = URLRequest(url: url) urlRequest.httpMethod = parameters.httpMethod @@ -61,7 +57,8 @@ final class MSALNativeAuthV2RequestConfigurator: MSIDAADRequestConfigurator { request.requestSerializer = MSALNativeAuthUrlRequestSerializer( context: parameters.context, - encoding: parameters.encoding + encoding: parameters.encoding, + body: parameters.body ) // Reuse the shared AAD request pipeline. From 00c6ccc0a3c13df329b4091a8fb71bf6ab3982e3 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:17:45 +0100 Subject: [PATCH 42/85] Updates to code guides --- .clinerules/04-Code-style-guidelines.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.clinerules/04-Code-style-guidelines.md b/.clinerules/04-Code-style-guidelines.md index b2c407dbec..532b57a65b 100644 --- a/.clinerules/04-Code-style-guidelines.md +++ b/.clinerules/04-Code-style-guidelines.md @@ -560,7 +560,7 @@ This style guide is adapted specifically for AI agents working on the Microsoft ## Swift Style (native_auth) -The Swift code under `MSAL/src/native_auth` (including V2 / server-driven flows) **MUST** follow the same formatting conventions already established by the V1 native auth code (e.g. `MSALNativeAuthSignInController.swift`, `MSALNativeAuthResetPasswordController.swift`, `MSALNativeAuthSignUpController.swift`). SwiftLint is enforced for this directory via `MSAL/.swiftlint.yml`. +The Swift code under `MSAL/src/native_auth`) **MUST** follow the SwiftLint rules from `MSAL/.swiftlint.yml`. ### SwiftLint configuration (source of truth: `MSAL/.swiftlint.yml`) @@ -578,10 +578,9 @@ swiftlint lint --quiet MSAL/src/native_auth/.swift ### Line length — WRAP, don't suppress -When a call or declaration exceeds 150 columns, **wrap it** — put each argument on its own line, indented 4 spaces beyond the call, with the closing paren on its own line. This matches V1 (see `MSALNativeAuthResetPasswordController.swift` `doPollCompletionLoop` / `handlePollCompletionResponse`). +When a call or declaration exceeds 150 columns, **wrap it** — put each argument on its own line, indented 4 spaces beyond the call, with the closing paren on its own line. ```swift -// Preferred (V1 style) return await mapInteraction( startResult, flowType: .signIn, @@ -609,12 +608,12 @@ return failure( ### Method / call declaration formatting -- One parameter per line when a declaration exceeds the line limit; closing paren and `-> ReturnType` on their own line (matches V1 and the repo-wide Objective-C convention). +- One parameter per line when a declaration exceeds the line limit; closing paren and `-> ReturnType` on their own line. - 4-space indentation, never tabs. ### function_body_length & cyclomatic_complexity — prefer suppression over refactor -V1 controllers routinely exceed the 50-line body limit for legitimately long orchestration methods and suppress the warning rather than fragmenting the logic. Follow the same pattern — do **not** refactor control flow purely to satisfy the linter. +Long orchestration methods that legitimately exceed the 50-line body limit should suppress the warning rather than fragmenting the logic across helpers. Do **not** refactor control flow purely to satisfy the linter. - Add the suppression on the line immediately above the `func`: From 0fd300f8ba49913336b73aa04814342a75b994e7 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:19:55 +0100 Subject: [PATCH 43/85] Removed rule from agents.md --- .clinerules/AGENTS.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.clinerules/AGENTS.md b/.clinerules/AGENTS.md index c70a7bcc11..364e72d24a 100644 --- a/.clinerules/AGENTS.md +++ b/.clinerules/AGENTS.md @@ -42,8 +42,6 @@ Sample code snippets for both Swift & Objective-C can be found in the file `.cli Code style guidelines that AI agents MUST follow when working with this repository can be found in the file `.clinerules/04-Code-style-guidelines.md` -For Swift code under `MSAL/src/native_auth` (including V2 / server-driven flows), follow the **Swift Style (native_auth)** section of that file: match the existing V1 native auth formatting, keep changed files SwiftLint-clean (`MSAL/.swiftlint.yml`), wrap long calls/declarations to ≤150 columns (one argument per line) rather than suppressing `line_length`, and prefer `// swiftlint:disable:next function_body_length` / `cyclomatic_complexity` over refactoring long orchestration methods. - ## Feature flag guidelines Feature flag guidance for AI agents when implementing new features for MSAL library are defined in the file `.clinerules/05-Feature-gating.md` From 5026f8d131db0e64cc4212bd0e52ee8d304ed847 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:42:14 +0100 Subject: [PATCH 44/85] removed not necessary imports suppressed sendable error --- .../controllers/MSALNativeAuthBaseController.swift | 2 -- .../native_auth/controllers/MSALNativeAuthTokenCacher.swift | 2 -- .../controllers/MSALNativeAuthTokenRequestHandling.swift | 5 ++--- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift index e0a43571b2..a9886ef86d 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift @@ -24,8 +24,6 @@ @_implementationOnly import MSAL_Private -import Foundation - class MSALNativeAuthBaseController { typealias TelemetryInfo = (event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext) diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift index 0cf9811f83..24a34a11df 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift @@ -24,8 +24,6 @@ @_implementationOnly import MSAL_Private -import Foundation - /// Token-response cache persistence. final class MSALNativeAuthTokenCacher { diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift index 879cd2e62f..73f18691a6 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift @@ -24,8 +24,6 @@ @_implementationOnly import MSAL_Private -import Foundation - protocol MSALNativeAuthTokenRequestHandling { func performTokenRequest( @@ -40,6 +38,7 @@ extension MSALNativeAuthTokenRequestHandling { _ request: MSIDHttpRequest, context: MSIDRequestContext ) async -> Result { + let requestCorrelationId = request.context?.correlationId().uuidString return await withCheckedContinuation { continuation in request.send { response, error in if let error = error { @@ -53,7 +52,7 @@ extension MSALNativeAuthTokenRequestHandling { do { let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) // use request correlation id if server doesn't return one - tokenResponse.correlationId = tokenResponse.correlationId ?? request.context?.correlationId().uuidString + tokenResponse.correlationId = tokenResponse.correlationId ?? requestCorrelationId continuation.resume(returning: .success(tokenResponse)) } catch { MSALNativeAuthLogger.log(level: .error, context: context, format: "Error token request - Both result and error are nil") From 96fca30e9bd91826fea6ea544c0f138df242ef7d Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:46:51 +0100 Subject: [PATCH 45/85] Avoid non-Sendable captures in native-auth request continuations Restructure performTokenRequest and performRequest so the withCheckedContinuation closure only bridges the send callback into async, moving response parsing, correlation-id handling, and logging out to the async body. This removes the Swift concurrency 'capture of non-Sendable type in a @Sendable closure' warnings for request/context/self without changing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../MSALNativeAuthBaseController.swift | 53 +++++++++++-------- .../MSALNativeAuthTokenRequestHandling.swift | 35 ++++++------ 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift index a9886ef86d..938c1068a3 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift @@ -163,35 +163,42 @@ class MSALNativeAuthBaseController { _ request: MSIDHttpRequest, context: MSALNativeAuthRequestContext ) async -> Result { - return await withCheckedContinuation { continuation in - request.send { [weak self] result, error in + let result: Result = await withCheckedContinuation { continuation in + request.send { response, error in if let error = error { - // 5xx errors contain the server's returned correlation-id in userInfo. - if let correlationId = self?.extractCorrelationIdFromUserInfo((error as NSError).userInfo) { - context.setServerCorrelationId(UUID(uuidString: correlationId)) - - // 4xx errors are decoded producing an error that conforms to MSALNativeAuthResponseCorrelatable protocol. - } else if let errorWithCorrelationId = error as? MSALNativeAuthResponseCorrelatable { - context.setServerCorrelationId(errorWithCorrelationId.correlationId) - - // If a 4xx error fails to decode, this error is returned from the error deserializer. - } else if case MSALNativeAuthInternalError.responseSerializationError(let correlationId) = error { - context.setServerCorrelationId(correlationId) - } else { - context.setServerCorrelationId(nil) - MSALNativeAuthLogger.log(level: .warning, context: context, format: "Error request - cannot decode error headers. Continuing") - } - continuation.resume(returning: .failure(error)) - } else if let response = result as? T { - context.setServerCorrelationId(response.correlationId) - continuation.resume(returning: .success(response)) } else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error request - Both result and error are nil") - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) + continuation.resume(returning: .success(response)) } } } + switch result { + case .failure(let error): + // 5xx errors contain the server's returned correlation-id in userInfo. + if let correlationId = extractCorrelationIdFromUserInfo((error as NSError).userInfo) { + context.setServerCorrelationId(UUID(uuidString: correlationId)) + + // 4xx errors are decoded producing an error that conforms to MSALNativeAuthResponseCorrelatable protocol. + } else if let errorWithCorrelationId = error as? MSALNativeAuthResponseCorrelatable { + context.setServerCorrelationId(errorWithCorrelationId.correlationId) + + // If a 4xx error fails to decode, this error is returned from the error deserializer. + } else if case MSALNativeAuthInternalError.responseSerializationError(let correlationId) = error { + context.setServerCorrelationId(correlationId) + } else { + context.setServerCorrelationId(nil) + MSALNativeAuthLogger.log(level: .warning, context: context, format: "Error request - cannot decode error headers. Continuing") + } + return .failure(error) + case .success(let response): + if let response = response as? T { + context.setServerCorrelationId(response.correlationId) + return .success(response) + } else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error request - Both result and error are nil") + return .failure(MSALNativeAuthInternalError.invalidResponse) + } + } } private func extractCorrelationIdFromUserInfo(_ userInfo: [String: Any]) -> String? { diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift index 73f18691a6..63e889afa2 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift @@ -39,26 +39,31 @@ extension MSALNativeAuthTokenRequestHandling { context: MSIDRequestContext ) async -> Result { let requestCorrelationId = request.context?.correlationId().uuidString - return await withCheckedContinuation { continuation in + let result: Result = await withCheckedContinuation { continuation in request.send { response, error in if let error = error { continuation.resume(returning: .failure(error)) - return - } - guard let responseDict = response as? [AnyHashable: Any] else { - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) - return - } - do { - let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) - // use request correlation id if server doesn't return one - tokenResponse.correlationId = tokenResponse.correlationId ?? requestCorrelationId - continuation.resume(returning: .success(tokenResponse)) - } catch { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error token request - Both result and error are nil") - continuation.resume(returning: .failure(MSALNativeAuthInternalError.invalidResponse)) + } else { + continuation.resume(returning: .success(response)) } } } + switch result { + case .failure(let error): + return .failure(error) + case .success(let response): + guard let responseDict = response as? [AnyHashable: Any] else { + return .failure(MSALNativeAuthInternalError.invalidResponse) + } + do { + let tokenResponse = try MSALNativeAuthCIAMTokenResponse(jsonDictionary: responseDict) + // use request correlation id if server doesn't return one + tokenResponse.correlationId = tokenResponse.correlationId ?? requestCorrelationId + return .success(tokenResponse) + } catch { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error token request - Both result and error are nil") + return .failure(MSALNativeAuthInternalError.invalidResponse) + } + } } } From 27cbbfba94b80a42d9c7eba8cd0eb0537f5aa37f Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:52:42 +0100 Subject: [PATCH 46/85] Removed not necessary imports Replaced long dashes --- .../controllers/v2/MSALNativeAuthFlowInternalState.swift | 2 +- .../v2/MSALNativeAuthFlowResponseDispatcher.swift | 2 +- .../network/MSALNativeAuthRequestInterceptorBridge.swift | 2 -- .../network/responses/v2/MSALNativeAuthHALResponse.swift | 2 -- .../v2/MSALNativeAuthV2HALResponseSerializer.swift | 4 +--- .../v2/MSALNativeAuthV2ResponseErrorHandler.swift | 4 +--- .../network/v2/MSALNativeAuthV2HrefURLResolver.swift | 2 +- .../network/v2/MSALNativeAuthV2RequestConfigurator.swift | 2 -- .../network/v2/MSALNativeAuthV2RequestProvider.swift | 2 -- .../state_machine/v2/MSALNativeAuthFlowDelegate.swift | 2 +- .../state_machine/v2/state/MSALNativeAuthState.swift | 8 ++++---- .../MSALNativeAuthEmailCodeRetriever.swift | 2 +- .../v2/MSALNativeAuthFlowControllerTests.swift | 2 +- 13 files changed, 12 insertions(+), 24 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift index 246d4481cb..5941158dad 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift @@ -31,7 +31,7 @@ import Foundation /// `submitCode(_:delegate:)`) forward to ``run(delegate:operation:)``, which invokes the matching /// controller operation and routes the resulting response back through the dispatcher. /// -/// This type carries no public API surface — apps interact only with the concrete +/// This type carries no public API surface - apps interact only with the concrete /// ``MSALNativeAuthState`` subclasses. class MSALNativeAuthFlowInternalState { diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift index 963182f38d..2aea9d19bf 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift @@ -28,7 +28,7 @@ import Foundation /// /// V2 uses opt-in, per-state delegate protocols that extend ``MSALNativeAuthFlowDelegate``. For an /// `actionRequired` result the dispatcher builds the concrete ``MSALNativeAuthState`` for the step, -/// wires it to the internal state, and — if the app's delegate conforms to that step's protocol — +/// wires it to the internal state, and - if the app's delegate conforms to that step's protocol - /// invokes its dedicated callback. If the app does not conform, the terminal /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. struct MSALNativeAuthFlowResponseDispatcher { diff --git a/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift b/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift index 06b84b114b..713d70cbca 100644 --- a/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift +++ b/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.swift @@ -22,8 +22,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private /// Bridges `MSALNativeAuthRequestInterceptor` (Swift public protocol) to `MSIDHttpRequestInterceptorProtocol` (ObjC). diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 3ed8e4c095..08900491f8 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -22,8 +22,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private /// Generic, server-driven HAL response used by all Native Auth V2 flows. diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index fc67508667..9cd9421e27 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -22,14 +22,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private /// Parses a raw HTTP response into a ``MSALNativeAuthHALResponse``. /// /// V2 responses are HAL+JSON and every HTTP outcome carries a meaningful body, so this -/// serializer never throws on a non-200 status — it captures the status code and lets the +/// serializer never throws on a non-200 status - it captures the status code and lets the /// V2 validator decide. HAL `_links` / `_embedded` extraction is delegated to the shared /// `HALResource`. final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSerialization { diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift index 7f5fc79977..ab98313c25 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.swift @@ -22,14 +22,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private /// Error handler for the Native Auth V2 transport pipeline. /// /// `MSIDHttpRequest` only routes HTTP 200 through the response serializer; every other -/// status code is delivered here. In V2 the meaningful body lives on every outcome — +/// status code is delivered here. In V2 the meaningful body lives on every outcome - /// the `401` from `authorize-challenge` carries the `continuation_token`, and /// `4xx` responses carry an `error` object. So this handler simply re-runs the HAL /// response serializer for any status and hands the parsed ``MSALNativeAuthHALResponse`` diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 27922abd18..53dc1d9680 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -57,7 +57,7 @@ struct MSALNativeAuthV2HrefURLResolver { /// Resolves a server-provided `_links` href into an absolute URL against the authority host. /// - /// The server returns hrefs whose leading path segment is a tenant identifier — typically the + /// The server returns hrefs whose leading path segment is a tenant identifier - typically the /// tenant **GUID** (e.g. `/{tenantId}/api/v0.1/signup/start`). However, the authorization challenge /// continuation_token is bound to the tenant form used by the authority /// (`.onmicrosoft.com`); calling the GUID path makes ESTS reject the token with diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift index 32da6669df..d76806b154 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -22,8 +22,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private /// Builds a fully-configured `MSIDHttpRequest` for any `MSALNativeAuthV2Requestable`. Subclassing diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index d670574b85..edcc95cdc9 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -22,8 +22,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private protocol MSALNativeAuthV2RequestProviding { diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift index 7f8c6c9a88..63a5eff989 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowDelegate.swift @@ -26,7 +26,7 @@ import Foundation /// Shared base delegate for all Native Auth V2 (server-driven) flows. /// -/// Unlike V1 — which exposes a different delegate protocol per step — V2 uses one +/// Unlike V1 - which exposes a different delegate protocol per step - V2 uses one /// family of delegates for sign up, sign in and reset password. The SDK drives the flow and /// reports back through these callbacks; the app reacts and continues the flow by /// calling methods directly on the ``MSALNativeAuthState`` it is handed. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift index 844e49a4b8..8244a74971 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift @@ -29,10 +29,10 @@ import Foundation /// In V2 the server drives the flow: at each step the SDK reports a concrete /// ``MSALNativeAuthState`` subclass through its dedicated ``MSALNativeAuthFlowDelegate`` callback /// (e.g. ``MSALNativeAuthCodeRequiredDelegate/onCodeRequired(state:scenario:)``). The app then continues the flow by -/// calling the method(s) exposed on that concrete state — each state exposes only the +/// calling the method(s) exposed on that concrete state - each state exposes only the /// continuations valid for its step, so invalid calls are impossible. /// -/// This is an abstract base class — the SDK always hands back one of its concrete subclasses to the +/// This is an abstract base class - the SDK always hands back one of its concrete subclasses to the /// matching state-specific delegate callback, so apps never need to downcast the state. /// /// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. @@ -41,11 +41,11 @@ public class MSALNativeAuthState: NSObject { /// The originating flow scenario for this state, set by the SDK when the state is created. /// Reported alongside this state's delegate callbacks so the app can tell which flow produced - /// it. Internal detail — not part of the public API surface. + /// it. Internal detail - not part of the public API surface. var scenario: MSALNativeAuthFlowScenario = .unknown /// The internal state that continues the server-driven flow from this state, injected by the SDK - /// when the state is created. Internal detail — not part of the public API surface. `nil` only for + /// when the state is created. Internal detail - not part of the public API surface. `nil` only for /// states an app constructs directly (which cannot advance a flow). var internalState: MSALNativeAuthFlowInternalState? diff --git a/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift b/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift index 6530459b55..d36723b114 100644 --- a/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift +++ b/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift @@ -129,7 +129,7 @@ class MSALNativeAuthEmailCodeRetriever { do { let (_, response) = try await URLSession.shared.data(for: request) let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0 - // 201 created, 422 already exists — both mean the mailbox is usable. + // 201 created, 422 already exists - both mean the mailbox is usable. guard statusCode == 201 || statusCode == 422 else { print("Failed to create mail.tm account: \(statusCode) status code") return nil diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index c6fcefdeab..d32056c803 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -335,7 +335,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { parameters.password = "Secret-Password-1" // Step 1: sign-up start. The server asks for `email`; the SDK auto-submits it and the app - // only sees the subsequent codeRequired action — never an attributesRequired for email. + // only sees the subsequent codeRequired action - never an attributesRequired for email. let startResponse = await sut.signUp(parameters: parameters) guard case .actionRequired(let startAction, let codeState) = startResponse.result, case .codeRequired = startAction else { return XCTFail("Expected codeRequired action, got \(startResponse.result)") From 839f384086bbcd477f27b01b1e47b4f64ca59d21 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:54:18 +0100 Subject: [PATCH 47/85] Renamed makeV2FlowController to makeFlowController --- .../factories/MSALNativeAuthControllerFactory.swift | 4 ++-- .../public/MSALNativeAuthPublicClientApplication.swift | 6 +++--- .../native_auth/mock/MSALNativeAuthFactoriesMocks.swift | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift index 0c7775c126..cbe7690aaf 100644 --- a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift +++ b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift @@ -28,7 +28,7 @@ protocol MSALNativeAuthControllerBuildable { func makeJITController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthJITControlling func makeResetPasswordController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthResetPasswordControlling func makeCredentialsController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthCredentialsControlling - func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling + func makeFlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling } final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { @@ -58,7 +58,7 @@ final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { return MSALNativeAuthCredentialsController(config: config, cacheAccessor: cacheAccessor) } - func makeV2FlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling { + func makeFlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling { return MSALNativeAuthFlowController(config: config, cacheAccessor: cacheAccessor) } } diff --git a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index 1997e44309..0acdcac1a8 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -274,7 +274,7 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic delegate: MSALNativeAuthFlowDelegate ) { Task { - let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let controller = controllerFactory.makeFlowController(cacheAccessor: cacheAccessor) let dispatcher = MSALNativeAuthFlowResponseDispatcher() let response = await controller.signUp(parameters: parameters) @@ -293,7 +293,7 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic delegate: MSALNativeAuthFlowDelegate ) { Task { - let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let controller = controllerFactory.makeFlowController(cacheAccessor: cacheAccessor) let dispatcher = MSALNativeAuthFlowResponseDispatcher() let response = await controller.signIn(parameters: parameters) @@ -312,7 +312,7 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic delegate: MSALNativeAuthFlowDelegate ) { Task { - let controller = controllerFactory.makeV2FlowController(cacheAccessor: cacheAccessor) + let controller = controllerFactory.makeFlowController(cacheAccessor: cacheAccessor) let dispatcher = MSALNativeAuthFlowResponseDispatcher() let response = await controller.resetPassword(parameters: parameters) diff --git a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift index 0e99b0a80d..03fc859f97 100644 --- a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift +++ b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift @@ -102,7 +102,7 @@ class MSALNativeAuthControllerFactoryMock: MSALNativeAuthControllerBuildable { return credentialsController } - func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { + func makeFlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { return v2FlowController } } @@ -150,7 +150,7 @@ class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuild return credentialsController } - func makeV2FlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { + func makeFlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { return v2FlowController } } From 6815fa5bc10e5d1dccdf345419b20a3d80b34b0b Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 11:56:11 +0100 Subject: [PATCH 48/85] Removed import --- .../controllers/v2/MSALNativeAuthFlowController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 38f1a80b74..1d52457781 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -22,8 +22,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import Foundation - @_implementationOnly import MSAL_Private // swiftlint:disable file_length From a8432ee285cb088f5dff04098ab8cffd77305e74 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 12:11:52 +0100 Subject: [PATCH 49/85] Route reset-password challenge/resend through server-driven mapInteraction Remove handleCodeRequired, which hardcoded the client-side assumption that a reset-password challenge or code resend can only ever yield a code-required state. In the server-driven (HATEOAS) V2 model the server dictates the next state, so resetPassword and resendCode now delegate to mapInteraction and follow whatever the server returns (e.g. MFA required), instead of rejecting non code-required responses as failures. The expected code-required path is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../v2/MSALNativeAuthFlowController.swift | 51 ++++--------------- 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 1d52457781..d13ba43645 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -35,6 +35,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private let tokenCacher: MSALNativeAuthTokenCacher private let kNumberOfTimesToRetryPollCompletionCall = 5 + // TODO: Confirm this is needed and server doesn't send private let pollIntervalNanoseconds: UInt64 = 1_500_000_000 // 1.5s init( @@ -87,10 +88,14 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // The APIs request attributes at specific parts of the SingUp process // so they must be carried privately for the whole flow + + // TODO: Remove Email, it should not be kept, but there's a bug on API side var autofillValues: [String: Any] = ["email": parameters.username] if let attributes = parameters.attributes { autofillValues.merge(attributes) { _, new in new } } + + // TODO: Confirm password needs to be sent as attribute if let password = parameters.password { autofillValues["password"] = password } @@ -235,14 +240,14 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, context: context) } - return handleCodeRequired( + return await mapInteraction( challengeResult, flowScenario: flowScenario, username: parameters.username, - fallbackHint: hint, scopes: scopes, event: event, - context: context + context: context, + fallbackHint: hint ) } @@ -622,14 +627,14 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati try self.requestProvider.challenge(href: resendHref, continuationToken: continuation.continuationToken, context: context) } - return handleCodeRequired( + return await mapInteraction( result, flowScenario: continuation.flowScenario, username: continuation.username, - fallbackHint: continuation.sentToHint, scopes: continuation.scopes, event: event, - context: context + context: context, + fallbackHint: continuation.sentToHint ) } @@ -679,40 +684,6 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MARK: - Result mapping - private func handleCodeRequired( - _ result: MSALNativeAuthV2InteractionValidatedResponse, - flowScenario: MSALNativeAuthFlowScenario, - username: String?, - fallbackHint: String?, - scopes: [String], - event: MSIDTelemetryAPIEvent?, - context: MSALNativeAuthRequestContext - ) -> MSALNativeAuthFlowControllerResponse { - switch result { - case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.verify: verifyHref, .resend: resendHref], - username: username, - sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, - codeLength: codeLength, - scopes: scopes - ) - let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo - stopTelemetryEvent(event, context: context) - return response( - .actionRequired( - action: .codeRequired(sentTo: displaySentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), - newState: newState - ), - context: context, scenario: flowScenario - ) - default: - return interactionFailure(result, event: event, context: context, scenario: flowScenario, newState: nil) - } - } - // Maps a validated interaction response onto a controller response (the unified, server-driven // branch used by sign in / sign up / MFA / JIT continuation steps). On a terminal `continue` // state it runs the completion (authorize-challenge → token) sequence. From 5dbbbe937ff181d8201392a0616d0b239f6af137 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 12:35:58 +0100 Subject: [PATCH 50/85] Route reset-password code verification through server-driven mapInteraction The passwordReset branch of submitCode hardcoded the client-side assumption that verifying a reset code can only yield an update-required (new password) state, rejecting everything else as a failure. In the server-driven V2 model the server dictates the next state, so it now delegates to mapInteraction and follows whatever the server returns (e.g. MFA required) after code verification. Existing outcomes are preserved: update-required maps to newPasswordRequired, an invalid code stays recoverable via recoverableState, and other responses still fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../v2/MSALNativeAuthFlowController.swift | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index d13ba43645..e46a341f85 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -292,31 +292,15 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let result = await performInteraction(context: context) { try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) } - switch result { - case .updateRequired(let token, let updateHref): - let newState = makeState( - continuation.flowScenario, - continuationToken: token, - links: [.update: updateHref], - username: continuation.username, - scopes: continuation.scopes - ) - stopTelemetryEvent(event, context: context) - return response( - .actionRequired(action: .newPasswordRequired, newState: newState), - context: context, - scenario: continuation.flowScenario) - case .error(let error): - // Recoverable: allow the app to retry with the same code-required state. - return interactionFailure( - result, - event: event, - context: context, - scenario: continuation.flowScenario, - newState: error.isInvalidCode ? state : nil) - default: - return interactionFailure(result, event: event, context: context, scenario: continuation.flowScenario, newState: nil) - } + return await mapInteraction( + result, + flowScenario: continuation.flowScenario, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context, + recoverableState: state + ) case .unknown: let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) return failure( From a0b18d8612a023923ae5f7b6c9f32dbc1060370a Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 14:56:40 +0100 Subject: [PATCH 51/85] MFA --- .../v2/MSALNativeAuthFlowController.swift | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index e46a341f85..df6797d0e9 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -514,32 +514,16 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati try self.requestProvider.challenge(href: challengeHref, continuationToken: continuation.continuationToken, context: context) } - switch result { - case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): - let newState = makeState( - continuation.flowScenario, - continuationToken: token, - links: [.verify: verifyHref, .resend: resendHref], - username: continuation.username, - sentToHint: sentTo.isEmpty ? continuation.sentToHint : sentTo, - codeLength: codeLength, - scopes: continuation.scopes - ) - stopTelemetryEvent(event, context: context) - return response(.actionRequired( - action: .mfaVerificationRequired(sentTo: sentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), - newState: newState - ), context: context, scenario: continuation.flowScenario) - default: - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - event: event, - context: context - ) - } + return await mapInteraction( + result, + flowScenario: continuation.flowScenario, + username: continuation.username, + scopes: continuation.scopes, + event: event, + context: context, + fallbackHint: continuation.sentToHint, + codeRequiredAsMFA: true + ) } } @@ -681,6 +665,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati context: MSALNativeAuthRequestContext, recoverableState: MSALNativeAuthFlowInternalState? = nil, fallbackHint: String? = nil, + codeRequiredAsMFA: Bool = false, signUpAutofillValues: [String: Any]? = nil, signUpAutofillSubmittedIds: Set = [] ) async -> MSALNativeAuthFlowControllerResponse { @@ -707,11 +692,12 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo + let channel = MSALNativeAuthChannelType(value: "email") + let action: MSALNativeAuthAction = codeRequiredAsMFA + ? .mfaVerificationRequired(sentTo: displaySentTo, channel: channel, codeLength: codeLength) + : .codeRequired(sentTo: displaySentTo, channel: channel, codeLength: codeLength) stopTelemetryEvent(event, context: context) - return response(.actionRequired( - action: .codeRequired(sentTo: displaySentTo, channel: MSALNativeAuthChannelType(value: "email"), codeLength: codeLength), - newState: newState - ), context: context, scenario: flowScenario) + return response(.actionRequired(action: action, newState: newState), context: context, scenario: flowScenario) case .passwordRequired(let token, let verifyHref): let newState = makeState( flowScenario, From 18a140849cd51033f5f1d51644cd90e8d9c14d75 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:02:56 +0100 Subject: [PATCH 52/85] Treat invalid continuation token as a general error The continuation token is SDK-managed internal state the app never sets, so a rejected token is not developer-actionable. Map it to generalError and drop the dedicated invalidContinuationToken error type, message, and public accessor. Also remove the now-unused delegateNotImplementedV2 and invalidContinuationToken message constants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../network/errors/MSALNativeAuthErrorMessage.swift | 2 -- .../validator/MSALNativeAuthV2ResponseValidator.swift | 6 ++++-- .../state_machine/v2/MSALNativeAuthFlowError.swift | 11 +---------- .../v2/MSALNativeAuthV2ResponseValidatorTests.swift | 4 ++-- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift b/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift index ceb577e229..1efa01d2a9 100644 --- a/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift +++ b/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift @@ -39,8 +39,6 @@ enum MSALNativeAuthErrorMessage { static let invalidUsername = "Invalid username" static let generalError = "General error" static let invalidCode = "Invalid code" - static let delegateNotImplementedV2 = "Delegate %@ is not implemented" - static let invalidContinuationToken = "Invalid continuation token" static let invalidChallenge = "Invalid challenge" static let invalidInput = "Invalid input" static let refreshTokenExpired = "Refresh token is expired" diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 4000bfd787..e4f7a99805 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -257,8 +257,10 @@ extension MSALNativeAuthV2ResponseValidator { if serverError.innerErrorCode == "invalidContinuationToken" { // An invalid OTP and an invalid continuation token share the inner code; the outer - // code disambiguates (invalidGrant => the supplied OTP was wrong). - type = serverError.code == "invalidGrant" ? .invalidCode : .invalidContinuationToken + // code disambiguates (invalidGrant => the supplied OTP was wrong). A rejected + // continuation token is SDK-managed internal state the app cannot act on, so it + // surfaces as a general error. + type = serverError.code == "invalidGrant" ? .invalidCode : .generalError } else if let message = message, message.contains("AADSTS50034") { type = .userNotFound } else if serverError.innerErrorCode == "invalidUserNameOrPassword" diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift index a65c9e3e40..6ff1eb9c54 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift @@ -42,8 +42,6 @@ public class MSALNativeAuthFlowError: MSALNativeAuthError { case userNotFound /// The submitted one-time code was invalid or expired. case invalidCode - /// The continuation token was rejected by the server. - case invalidContinuationToken /// The submitted password did not meet the server's requirements. case invalidPassword /// The username and/or password supplied at sign in were not accepted by the server. @@ -113,13 +111,11 @@ public class MSALNativeAuthFlowError: MSALNativeAuthError { switch type { case .notImplemented: - return MSALNativeAuthErrorMessage.delegateNotImplementedV2 + return MSALNativeAuthErrorMessage.delegateNotImplemented case .userNotFound: return MSALNativeAuthErrorMessage.userNotFound case .invalidCode: return MSALNativeAuthErrorMessage.invalidCode - case .invalidContinuationToken: - return MSALNativeAuthErrorMessage.invalidContinuationToken case .invalidPassword: return MSALNativeAuthErrorMessage.invalidPassword case .invalidCredentials: @@ -160,11 +156,6 @@ public class MSALNativeAuthFlowError: MSALNativeAuthError { return type == .invalidCode } - /// Whether the continuation token was rejected by the server. - public var isInvalidContinuationToken: Bool { - return type == .invalidContinuationToken - } - /// Whether the submitted password was rejected because it did not satisfy the server's /// policy during sign up. public var isInvalidPassword: Bool { diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 56d4fb5b09..0a7eebfdb4 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -204,11 +204,11 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCode))) } - func test_validateInteraction_invalidContinuationToken_mapsCorrectly() { + func test_validateInteraction_invalidContinuationToken_mapsToGeneralError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad token", innerErrorCode: "invalidContinuationToken", correlationId: nil) let response = makeResponse(error: serverError) let result = sut.validateInteraction(context: context, .success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidContinuationToken))) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } // MARK: - validateToken From 606a0beae480d2645cebc45ea21e5e0ee4f0053a Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:06:49 +0100 Subject: [PATCH 53/85] Comments --- .../network/responses/v2/MSALNativeAuthHALResponse.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 08900491f8..59549bdeca 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -72,10 +72,10 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let hint: String? /// The authentication factor required by a `challenge` action, taken from - /// `challengeContext.authenticationFactor` (e.g. "multiFactor" when MFA is required). + /// `challengeContext.authenticationFactor` let authenticationFactor: String? - /// Top-level method identifier (`id`) on method-style responses (sign up `start`, JIT `activate`). + /// Top-level method identifier (`id`) on method-style responses let methodId: String? /// Top-level method type (`type`, e.g. "email") on method-style responses. let methodType: String? From c69737ee0be3e92046538b201a77eed17c811c8b Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:09:01 +0100 Subject: [PATCH 54/85] Comment --- .../responses/v2/MSALNativeAuthV2HALResponseSerializer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 9cd9421e27..aa58eddf86 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -37,7 +37,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali let correlationId = MSALNativeAuthHALResponse.retrieveCorrelationIdFromHeaders(from: httpResponse) guard let data = data, !data.isEmpty else { - // An empty body with a success status is still a valid (terminal) response. + // an empty body is wrapped as an empty response, the validator rejects it as a general error. return MSALNativeAuthHALResponse( statusCode: statusCode, correlationId: correlationId, From 1bbbdab83ca7b3d9c65c174cd96440b5ea365fbc Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:16:37 +0100 Subject: [PATCH 55/85] Remove unused V2 HAL token validator The V2 HAL validateToken path had no production callers. The real V2 token exchange (completeWithToken) already decodes the /token response into an MSIDTokenResponse via the raw-JSON serializer and reuses the shared MSALNativeAuthTokenCacher and MSIDTokenResponseValidator, mirroring V1. Removes the dead validateToken protocol method and implementation, the MSALNativeAuthV2TokenValidatedResponse enum, the orphaned accessToken field on MSALNativeAuthHALResponse, and the associated mock and tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../v2/MSALNativeAuthHALResponse.swift | 2 -- ...SALNativeAuthV2HALResponseSerializer.swift | 2 -- .../MSALNativeAuthV2ResponseValidator.swift | 20 ----------------- .../MSALNativeAuthV2ValidatedResponses.swift | 6 ----- ...SALNativeAuthV2ResponseValidatorMock.swift | 10 --------- ...ALNativeAuthV2ResponseValidatorTests.swift | 22 ------------------- 6 files changed, 62 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 59549bdeca..608516ea28 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -84,8 +84,6 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { /// Authorization code from the final `authorize-challenge` call. let code: String? - /// Access token from the `/token` exchange. - let accessToken: String? /// Top-level `_links`, keyed by relation, value is the raw href string. let links: [String: String] diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index aa58eddf86..bf61ba4512 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -51,7 +51,6 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali methodType: nil, attributes: [], code: nil, - accessToken: nil, links: [:], methods: [], error: nil @@ -78,7 +77,6 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali methodType: resource.string(forKey: "type"), attributes: parseAttributes(from: json), code: resource.string(forKey: "code"), - accessToken: resource.string(forKey: "access_token"), links: parseLinks(from: resource, json: json), methods: parseMethods(from: resource), error: parseError(from: json, fallbackCorrelationId: correlationId) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index e4f7a99805..933929a8b7 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -35,10 +35,6 @@ protocol MSALNativeAuthV2ResponseValidating { context: MSIDRequestContext, _ result: Result ) -> MSALNativeAuthV2InteractionValidatedResponse - func validateToken( - context: MSIDRequestContext, - _ result: Result - ) -> MSALNativeAuthV2TokenValidatedResponse } final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidating { @@ -215,22 +211,6 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin } } } - - func validateToken( - context: MSIDRequestContext, - _ result: Result - ) -> MSALNativeAuthV2TokenValidatedResponse { - switch result { - case .failure(let error): - return .error(flowError(from: error, context: context)) - case .success(let response): - if let error = response.error { - return .error(flowError(from: error, context: context)) - } - MSALNativeAuthLogger.log(level: .verbose, context: context, format: "token: exchange succeeded") - return .success(accessToken: response.accessToken) - } - } } extension MSALNativeAuthV2ResponseValidator { diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index b67648c8b4..14c11dc840 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -109,9 +109,3 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { } } } - -/// Validated outcome of the `/token` exchange. -enum MSALNativeAuthV2TokenValidatedResponse { - case success(accessToken: String?) - case error(MSALNativeAuthFlowError) -} diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift index 9a2b0ee003..92c3b71827 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift @@ -30,11 +30,9 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating var authorizeChallengeResponses: [MSALNativeAuthV2AuthorizeChallengeValidatedResponse] = [] var interactionResponses: [MSALNativeAuthV2InteractionValidatedResponse] = [] - var tokenResponse: MSALNativeAuthV2TokenValidatedResponse = .error(MSALNativeAuthFlowError(type: .generalError)) private(set) var validateAuthorizeChallengeCallCount = 0 private(set) var validateInteractionCallCount = 0 - private(set) var validateTokenCallCount = 0 func validateAuthorizeChallenge( context: MSIDRequestContext, @@ -58,12 +56,4 @@ class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating } return .error(MSALNativeAuthFlowError(type: .generalError)) } - - func validateToken( - context: MSIDRequestContext, - _ result: Result - ) -> MSALNativeAuthV2TokenValidatedResponse { - validateTokenCallCount += 1 - return tokenResponse - } } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 0a7eebfdb4..4d26103153 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -51,7 +51,6 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { methodType: String? = nil, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry] = [], code: String? = nil, - accessToken: String? = nil, links: [String: String] = [:], methods: [MSALNativeAuthHALResponse.EmbeddedMethod] = [], error: MSALNativeAuthHALResponse.ServerError? = nil @@ -69,7 +68,6 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { methodType: methodType, attributes: attributes, code: code, - accessToken: accessToken, links: links, methods: methods, error: error @@ -210,24 +208,4 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - - // MARK: - validateToken - - func test_validateToken_success() { - let response = makeResponse(accessToken: "access-token") - let result = sut.validateToken(context: context, .success(response)) - guard case .success(let accessToken) = result else { - return XCTFail("Expected success") - } - XCTAssertEqual(accessToken, "access-token") - } - - func test_validateToken_withServerError_returnsError() { - let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "bad", innerErrorCode: nil, correlationId: nil) - let response = makeResponse(error: serverError) - let result = sut.validateToken(context: context, .success(response)) - guard case .error = result else { - return XCTFail("Expected error") - } - } } From 2fa2181491706220cdd8398729536ffe46fce1cb Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:27:29 +0100 Subject: [PATCH 56/85] Fold sign-in method discovery into the interaction action switch Move the nil-action sign-in method discovery check into the default arm of the halAction switch, where it naturally belongs alongside the unexpected-action handling. Behavior is unchanged: the raw action check still distinguishes a nil action (method discovery) from an unknown action string (error). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../MSALNativeAuthV2ResponseValidator.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 933929a8b7..722c43c055 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -105,16 +105,6 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in interaction response")) } - // Sign-in method discovery: no action, but the available methods are embedded. - if response.action == nil, !response.methods.isEmpty { - MSALNativeAuthLogger.log( - level: .verbose, - context: context, - format: "interaction: returning %d sign-in methods", - response.methods.count) - return .signInMethods(continuationToken: continuationToken, methods: response.methods) - } - MSALNativeAuthLogger.log(level: .verbose, context: context, format: "interaction: processing action '%@'", response.action ?? "nil") switch response.halAction { @@ -206,6 +196,15 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin pollHref: pollHref ) default: + // No recognized action. A nil action with embedded methods is sign-in method discovery. + if response.action == nil, !response.methods.isEmpty { + MSALNativeAuthLogger.log( + level: .verbose, + context: context, + format: "interaction: returning %d sign-in methods", + response.methods.count) + return .signInMethods(continuationToken: continuationToken, methods: response.methods) + } MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: unexpected action '%@'", response.action ?? "nil") return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) } From 94389889f986861cf91ed255e64fd4fc0b6cf4de Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:34:50 +0100 Subject: [PATCH 57/85] Comments --- .../MSALNativeAuthV2ResponseValidator.swift | 2 -- .../v2/MSALNativeAuthV2HrefURLResolver.swift | 13 ------------- 2 files changed, 15 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 722c43c055..7e91d682ff 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -282,8 +282,6 @@ extension MSALNativeAuthV2ResponseValidator { MSALLogMask.maskPII(serverError.message)) } - /// Extracts the numeric ESTS error codes (e.g. `50126` from `AADSTS50126`) embedded in a - /// server error message. private func estsErrorCodes(from message: String?) -> [Int] { guard let message = message else { return [] diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 53dc1d9680..96ad172088 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -56,14 +56,6 @@ struct MSALNativeAuthV2HrefURLResolver { } /// Resolves a server-provided `_links` href into an absolute URL against the authority host. - /// - /// The server returns hrefs whose leading path segment is a tenant identifier - typically the - /// tenant **GUID** (e.g. `/{tenantId}/api/v0.1/signup/start`). However, the authorization challenge - /// continuation_token is bound to the tenant form used by the authority - /// (`.onmicrosoft.com`); calling the GUID path makes ESTS reject the token with - /// AADSTS55200 ("continuation_token is invalid"). To keep the tenant identifier consistent for - /// the whole flow, the leading tenant segment is dropped and the remaining API path is grafted - /// onto the authority's path, reproducing the URL against the configured authority. func url(forHref href: String) throws -> URL { let trimmed = href.trimmingCharacters(in: .whitespacesAndNewlines) @@ -103,11 +95,6 @@ struct MSALNativeAuthV2HrefURLResolver { } /// Returns the API portion of a server href path, dropping any leading tenant segment. - /// - /// Native Auth V2 hrefs are of the form `//api/v0.1/...` (the tenant being a GUID or - /// `.onmicrosoft.com`). We key off the first known API marker and keep the path from - /// there, so the tenant segment is removed regardless of its form. Hrefs that are already - /// host-relative (no tenant prefix) are returned unchanged (with a guaranteed leading slash). private func apiPath(from path: String) -> String { for marker in ["/api/", "/oauth2/"] { if let range = path.range(of: marker) { From 8ec1d8ac93f2ee441e095ed62f977bb214341afc Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:36:08 +0100 Subject: [PATCH 58/85] Comments --- .../native_auth/network/v2/MSALNativeAuthV2RequestBody.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift index a99ddeec92..55ce8572a1 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift @@ -26,8 +26,7 @@ import Foundation /// Typed body of a Native Auth V2 (HAL) follow-up request. Each field maps to a /// ``MSALNativeAuthV2RequestBodyKey`` and is emitted only when set, so a caller states just the -/// fields the operation carries (e.g. a password submit sets `password`, a poll sets nothing extra). -/// Owning the key mapping here keeps the request provider free of raw body dictionaries. +/// fields the operation carries struct MSALNativeAuthV2RequestBody { let continuationToken: String var password: String? From 48727b8b8e0c53c08ca32a1da29cda455a579314 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:36:51 +0100 Subject: [PATCH 59/85] Comments --- .../native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift index a3df6d8519..0f58ae68cb 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift @@ -28,7 +28,7 @@ import Foundation /// /// V2 HAL bodies are camelCase JSON (unlike the snake_case, form-encoded keys in /// ``MSALNativeAuthRequestParametersKey`` used by the OAuth `/token` and `/authorize-challenge` -/// endpoints). Centralizing these keys keeps the request bodies free of scattered string literals. +/// endpoints) enum MSALNativeAuthV2RequestBodyKey: String { case username case continuationToken From 57547e6ea2481887fcdb7230a1bafd4684ae7ccc Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:39:38 +0100 Subject: [PATCH 60/85] Comment --- .../v2/parameters/MSALNativeAuthV2TokenParameters.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift index 41dd51f7e4..5e16e882ab 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift @@ -24,8 +24,7 @@ import Foundation -/// `POST /token` authorization-code exchange. Form encoded, raw OAuth (non-HAL) response. Includes -/// `client_info=true` so ESTS returns the `client_info` blob required to persist tokens to the cache. +/// `POST /token` authorization-code exchange. Form encoded, raw OAuth (non-HAL) response. struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { let context: MSALNativeAuthRequestContext let clientId: String From 2945caf7066bab7a116e90bd690cdacb38204873 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:45:11 +0100 Subject: [PATCH 61/85] comment --- .../public/MSALNativeAuthPublicClientApplication.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index 0acdcac1a8..e57cc6d06c 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -232,7 +232,7 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic } } - /// Reset the password using parameters. + /// Reset the password using parameters /// - Parameters: /// - parameters: Parameters used for the Reset Password flow. /// - delegate: Delegate that receives callbacks for the Reset Password flow. From af477fa9b5668e2ecc10d54b47547ebee1ff5713 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 22 Jul 2026 15:57:26 +0100 Subject: [PATCH 62/85] Map password policy and credentials errors in V2 flow error mapper A passwordTooWeak server error was falling through to generalError. Map the passwordTooWeak inner error code to invalidPassword, and correct the invalidUserNameOrPassword / AADSTS50126 sign-in error to invalidCredentials (previously invalidPassword), giving each documented error type a distinct producer. Adds regression tests for both mappings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../MSALNativeAuthV2ResponseValidator.swift | 9 +++++--- ...ALNativeAuthV2ResponseValidatorTests.swift | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 7e91d682ff..e3a0f49ffe 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -232,9 +232,10 @@ extension MSALNativeAuthV2ResponseValidator { private func flowError(from serverError: MSALNativeAuthHALResponse.ServerError, context: MSIDRequestContext) -> MSALNativeAuthFlowError { let message = serverError.message let errorCodes = estsErrorCodes(from: message) + let innerErrorCode = serverError.innerErrorCode let type: MSALNativeAuthFlowError.ErrorType - if serverError.innerErrorCode == "invalidContinuationToken" { + if innerErrorCode == "invalidContinuationToken" { // An invalid OTP and an invalid continuation token share the inner code; the outer // code disambiguates (invalidGrant => the supplied OTP was wrong). A rejected // continuation token is SDK-managed internal state the app cannot act on, so it @@ -242,11 +243,13 @@ extension MSALNativeAuthV2ResponseValidator { type = serverError.code == "invalidGrant" ? .invalidCode : .generalError } else if let message = message, message.contains("AADSTS50034") { type = .userNotFound - } else if serverError.innerErrorCode == "invalidUserNameOrPassword" + } else if innerErrorCode == "passwordTooWeak" { + type = .invalidPassword + } else if innerErrorCode == "invalidUserNameOrPassword" || errorCodes.contains(MSALNativeAuthESTSApiErrorCodes.invalidCredentials.rawValue) { // Wrong username/password at sign in (AADSTS50126): a recoverable credentials error, // not an invalid one-time code. - type = .invalidPassword + type = .invalidCredentials } else if serverError.code == "invalidGrant" { type = .invalidCode } else { diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 4d26103153..fd93674eed 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -208,4 +208,26 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { let result = sut.validateInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } + + func test_validateInteraction_passwordTooWeak_mapsToInvalidPassword() { + let serverError = MSALNativeAuthHALResponse.ServerError( + code: "invalidRequest", + message: "AADSTS120002: New password doesn't meet complexity requirements.", + innerErrorCode: "passwordTooWeak", + correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidPassword))) + } + + func test_validateInteraction_invalidUserNameOrPassword_mapsToInvalidCredentials() { + let serverError = MSALNativeAuthHALResponse.ServerError( + code: "invalidGrant", + message: "AADSTS50126: Error validating credentials.", + innerErrorCode: "invalidUserNameOrPassword", + correlationId: nil) + let response = makeResponse(error: serverError) + let result = sut.validateInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCredentials))) + } } From 948f61bb4098878c78dacae772b71d3d2c0e7c76 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 23 Jul 2026 12:04:53 +0100 Subject: [PATCH 63/85] Replace native-auth V2 action enum with concrete states and server-driven channel Remove the MSALNativeAuthAction DTO in favour of building concrete MSALNativeAuthState subclasses in the controller, transported as a single self-contained object. States are fully formed via init(internalState:), and the dispatcher resolves the delegate callback via a type switch. Single-source the flow scenario: the response wrapper carries it for the state-less results (completed/error/browserRequired), while .actionRequired reads it from the state's continuation, so it is never stored twice. Source the code channel type from the server 'type' field (methodType) instead of hardcoding "email" for the code- and activation-required states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- MSAL/MSAL.xcodeproj/project.pbxproj | 6 -- .../v2/MSALNativeAuthFlowController.swift | 68 ++++++++----- ...MSALNativeAuthFlowResponseDispatcher.swift | 98 ++++++++----------- .../v2/MSALNativeAuthFlowResult.swift | 2 +- .../MSALNativeAuthV2ResponseValidator.swift | 2 + .../MSALNativeAuthV2ValidatedResponses.swift | 12 +-- .../v2/MSALNativeAuthAction.swift | 69 ------------- ...MSALNativeAuthAttributesInvalidState.swift | 4 +- ...SALNativeAuthAttributesRequiredState.swift | 4 +- .../MSALNativeAuthCodeRequiredState.swift | 7 +- .../MSALNativeAuthMFARequiredState.swift | 4 +- ...tiveAuthMFAVerificationRequiredState.swift | 4 +- .../v2/state/MSALNativeAuthState.swift | 30 ++---- ...hStrongAuthRegistrationRequiredState.swift | 4 +- ...hStrongAuthVerificationRequiredState.swift | 4 +- .../MSALNativeAuthFlowControllerTests.swift | 46 ++++----- ...ALNativeAuthV2ResponseValidatorTests.swift | 16 ++- 17 files changed, 156 insertions(+), 224 deletions(-) delete mode 100644 MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index f4a6986f43..a1fe4816bd 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -1037,7 +1037,6 @@ B2FBB3DB28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FE601B20E5BB5800502BA6 /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; - C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; @@ -1775,7 +1774,6 @@ E2F626B32A781CE300C4A303 /* SignInDelegatesSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F626B22A781CE300C4A303 /* SignInDelegatesSpies.swift */; }; E2F890052B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */; }; E2F8900E2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */; }; - E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */; }; E68C311BD4DDECABFAA212FD /* MSALNativeAuthFlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */; }; F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; @@ -2465,7 +2463,6 @@ A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAuthSchemeTests.m; sourceTree = ""; }; A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDevicePopManagerUtil.m; sourceTree = ""; }; A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; - AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAction.swift; sourceTree = ""; }; B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProvider.swift; sourceTree = ""; }; B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenCacher.swift; sourceTree = ""; }; B203459221AF77FB00B221AA /* MSALRedirectUri.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALRedirectUri.h; sourceTree = ""; }; @@ -3690,7 +3687,6 @@ B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, 58F36BCB7532CC3D9D60EE3A /* state */, - AA5A7F5C7D0876CAB2FE0433 /* MSALNativeAuthAction.swift */, ); path = v2; sourceTree = ""; @@ -7484,7 +7480,6 @@ E2EFAD162A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift in Sources */, 285F58542C5BA33B00F4EFA4 /* MSALNativeAuthSignInIntrospectRequestParameters.swift in Sources */, 28DE70D629FAC16700EB75AA /* MSALNativeAuthSignInResponseValidator.swift in Sources */, - C2A00B4744CF007AE1A4C623 /* MSALNativeAuthAction.swift in Sources */, 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowInternalState.swift in Sources */, 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthFlowResult.swift in Sources */, C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */, @@ -7819,7 +7814,6 @@ DE8DC4612C66219600534E8F /* SignUpResults.swift in Sources */, DE8DC4972C6621A600534E8F /* SignInAfterResetPasswordDelegate.swift in Sources */, DE8DC4512C66218900534E8F /* MSALNativeAuthInternalError.swift in Sources */, - E57BCB60D1A1378846A722ED /* MSALNativeAuthAction.swift in Sources */, 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */, 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */, 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index df6797d0e9..0d8e581f68 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -679,7 +679,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati event: event, context: context ) - case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let codeLength): + case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let channelType, let codeLength): let newState = makeState( flowScenario, continuationToken: token, @@ -692,12 +692,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo - let channel = MSALNativeAuthChannelType(value: "email") - let action: MSALNativeAuthAction = codeRequiredAsMFA - ? .mfaVerificationRequired(sentTo: displaySentTo, channel: channel, codeLength: codeLength) - : .codeRequired(sentTo: displaySentTo, channel: channel, codeLength: codeLength) + let state: MSALNativeAuthState = codeRequiredAsMFA + ? MSALNativeAuthMFAVerificationRequiredState(internalState: newState, sentTo: displaySentTo, channel: channelType, codeLength: codeLength) + : MSALNativeAuthCodeRequiredState(internalState: newState, sentTo: displaySentTo, channel: channelType, codeLength: codeLength) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: action, newState: newState), context: context, scenario: flowScenario) + return response(.actionRequired(state: state), context: context) case .passwordRequired(let token, let verifyHref): let newState = makeState( flowScenario, @@ -709,7 +708,9 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .passwordRequired, newState: newState), context: context, scenario: flowScenario) + return response( + .actionRequired(state: MSALNativeAuthPasswordRequiredState(internalState: newState)), + context: context) case .updateRequired(let token, let updateHref): let newState = makeState( flowScenario, @@ -721,7 +722,9 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired(action: .newPasswordRequired, newState: newState), context: context, scenario: flowScenario) + return response( + .actionRequired(state: MSALNativeAuthNewPasswordRequiredState(internalState: newState)), + context: context) case .attributesRequired(let token, let attributes, let submitHref): // Sign-up: submit values the app supplied at start (e.g. email/password) automatically. // The full set is kept intact for the whole flow; only the attributes the server @@ -765,10 +768,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati signUpAutofillSubmittedIds: signUpAutofillSubmittedIds ) stopTelemetryEvent(event, context: context) - return response(.actionRequired( - action: .attributesRequired(attributes: requiredAttributes(from: attributes)), - newState: newState - ), context: context, scenario: flowScenario) + return response( + .actionRequired( + state: MSALNativeAuthAttributesRequiredState(internalState: newState, attributes: requiredAttributes(from: attributes)) + ), + context: context) case .mfaRequired(let token, let methods, let challengeHref): let (authMethods, methodLinks) = authMethods(from: methods) let newState = makeState( @@ -784,9 +788,8 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) stopTelemetryEvent(event, context: context) return response( - .actionRequired(action: .mfaRequired(authMethods: authMethods), newState: newState), - context: context, - scenario: flowScenario) + .actionRequired(state: MSALNativeAuthMFARequiredState(internalState: newState, authMethods: authMethods)), + context: context) case .registrationRequired(let token, let enrollHref, let methods): let (authMethods, methodLinks) = authMethods(from: methods) let newState = makeState( @@ -802,10 +805,9 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) stopTelemetryEvent(event, context: context) return response( - .actionRequired(action: .strongAuthRegistrationRequired(authMethods: authMethods), newState: newState), - context: context, - scenario: flowScenario) - case .activationRequired(let token, let activateHref, let sentTo, let codeLength): + .actionRequired(state: MSALNativeAuthStrongAuthRegistrationRequiredState(internalState: newState, authMethods: authMethods)), + context: context) + case .activationRequired(let token, let activateHref, let sentTo, let channelType, let codeLength): let newState = makeState( flowScenario, continuationToken: token, @@ -819,14 +821,16 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo stopTelemetryEvent(event, context: context) - return response(.actionRequired( - action: .strongAuthVerificationRequired( - sentTo: displaySentTo, - channel: MSALNativeAuthChannelType(value: "email"), - codeLength: codeLength + return response( + .actionRequired( + state: MSALNativeAuthStrongAuthVerificationRequiredState( + internalState: newState, + sentTo: displaySentTo, + channel: channelType, + codeLength: codeLength + ) ), - newState: newState - ), context: context, scenario: flowScenario) + context: context) case .error(let error): stopTelemetryEvent(event, context: context, error: error) return response( @@ -1048,6 +1052,18 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } + /// Builds a response for `.actionRequired` results, whose scenario the dispatcher reads from the + /// state's continuation rather than the wrapper. + private func response( + _ result: MSALNativeAuthFlowResult, + context: MSALNativeAuthRequestContext + ) -> MSALNativeAuthFlowControllerResponse { + return MSALNativeAuthFlowControllerResponse( + result, + correlationId: context.correlationId() + ) + } + private func failure( _ validated: MSALNativeAuthV2AuthorizeChallengeValidatedResponse, event: MSIDTelemetryAPIEvent?, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift index 2aea9d19bf..f6ac38f61a 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift @@ -27,17 +27,17 @@ import Foundation /// Routes a controller response to the appropriate ``MSALNativeAuthFlowDelegate`` callback. /// /// V2 uses opt-in, per-state delegate protocols that extend ``MSALNativeAuthFlowDelegate``. For an -/// `actionRequired` result the dispatcher builds the concrete ``MSALNativeAuthState`` for the step, -/// wires it to the internal state, and - if the app's delegate conforms to that step's protocol - -/// invokes its dedicated callback. If the app does not conform, the terminal +/// `actionRequired` result the dispatcher matches the concrete ``MSALNativeAuthState`` to its +/// per-state delegate protocol and - if the app's delegate conforms - invokes its dedicated +/// callback. If the app does not conform, the terminal /// ``MSALNativeAuthFlowDelegate/onFlowError(error:scenario:)`` is called with error type `notImplemented`. struct MSALNativeAuthFlowResponseDispatcher { func dispatch(_ response: MSALNativeAuthFlowControllerResponse, delegate: MSALNativeAuthFlowDelegate) async { let scenario = response.scenario switch response.result { - case .actionRequired(let action, let internalState): - await dispatchAction(action, internalState: internalState, scenario: scenario, response: response, delegate: delegate) + case .actionRequired(let state): + await dispatchActionRequired(state, response: response, delegate: delegate) case .completed(let result): await delegate.onFlowCompleted(result: result, scenario: scenario) response.telemetryUpdate?(.success(())) @@ -54,75 +54,63 @@ struct MSALNativeAuthFlowResponseDispatcher { } } - private func dispatchAction( - _ action: MSALNativeAuthAction, - internalState: MSALNativeAuthFlowInternalState, - scenario: MSALNativeAuthFlowScenario, + private func dispatchActionRequired( + _ state: MSALNativeAuthState, response: MSALNativeAuthFlowControllerResponse, delegate: MSALNativeAuthFlowDelegate ) async { - switch action { - case .codeRequired(let sentTo, let channel, let codeLength): - let state = MSALNativeAuthCodeRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthCodeRequiredDelegate.self) { await $0.onCodeRequired(state: state, scenario: scenario) } - case .passwordRequired: - let state = MSALNativeAuthPasswordRequiredState() - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthPasswordRequiredDelegate.self) { await $0.onPasswordRequired(state: state, scenario: scenario) } - case .newPasswordRequired: - let state = MSALNativeAuthNewPasswordRequiredState() - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthNewPasswordRequiredDelegate.self) { await $0.onNewPasswordRequired(state: state, scenario: scenario) } - case .attributesRequired(let attributes): - let state = MSALNativeAuthAttributesRequiredState(attributes: attributes) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthAttributesRequiredDelegate.self) { + let scenario = state.internalState.continuation.flowScenario + switch state { + case let state as MSALNativeAuthCodeRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthCodeRequiredDelegate.self, scenario: scenario) { + await $0.onCodeRequired(state: state, scenario: scenario) + } + case let state as MSALNativeAuthPasswordRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthPasswordRequiredDelegate.self, scenario: scenario) { + await $0.onPasswordRequired(state: state, scenario: scenario) + } + case let state as MSALNativeAuthNewPasswordRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthNewPasswordRequiredDelegate.self, scenario: scenario) { + await $0.onNewPasswordRequired(state: state, scenario: scenario) + } + case let state as MSALNativeAuthAttributesRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthAttributesRequiredDelegate.self, scenario: scenario) { await $0.onAttributesRequired(state: state, scenario: scenario) } - case .attributesInvalid(let attributeNames): - let state = MSALNativeAuthAttributesInvalidState(attributeNames: attributeNames) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthAttributesInvalidDelegate.self) { await $0.onAttributesInvalid(state: state, scenario: scenario) } - case .mfaRequired(let authMethods): - let state = MSALNativeAuthMFARequiredState(authMethods: authMethods) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthMFARequiredDelegate.self) { await $0.onMFARequired(state: state, scenario: scenario) } - case .mfaVerificationRequired(let sentTo, let channel, let codeLength): - let state = MSALNativeAuthMFAVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthMFAVerificationRequiredDelegate.self) { + case let state as MSALNativeAuthAttributesInvalidState: + await deliver(to: delegate, response: response, as: MSALNativeAuthAttributesInvalidDelegate.self, scenario: scenario) { + await $0.onAttributesInvalid(state: state, scenario: scenario) + } + case let state as MSALNativeAuthMFARequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthMFARequiredDelegate.self, scenario: scenario) { + await $0.onMFARequired(state: state, scenario: scenario) + } + case let state as MSALNativeAuthMFAVerificationRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthMFAVerificationRequiredDelegate.self, scenario: scenario) { await $0.onMFAVerificationRequired(state: state, scenario: scenario) } - case .strongAuthRegistrationRequired(let authMethods): - let state = MSALNativeAuthStrongAuthRegistrationRequiredState(authMethods: authMethods) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthStrongAuthRegistrationRequiredDelegate.self) { + case let state as MSALNativeAuthStrongAuthRegistrationRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthStrongAuthRegistrationRequiredDelegate.self, scenario: scenario) { await $0.onStrongAuthRegistrationRequired(state: state, scenario: scenario) } - case .strongAuthVerificationRequired(let sentTo, let channel, let codeLength): - let state = MSALNativeAuthStrongAuthVerificationRequiredState(sentTo: sentTo, channel: channel, codeLength: codeLength) - await deliver(state, internalState: internalState, scenario: scenario, response: response, delegate: delegate, - as: MSALNativeAuthStrongAuthVerificationRequiredDelegate.self) { + case let state as MSALNativeAuthStrongAuthVerificationRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthStrongAuthVerificationRequiredDelegate.self, scenario: scenario) { await $0.onStrongAuthVerificationRequired(state: state, scenario: scenario) } + default: + await notImplemented(delegate: delegate, scenario: scenario, correlationId: response.correlationId) } } - /// Wires a concrete state to its internal state and originating scenario, then invokes the app's - /// per-state callback when the delegate conforms to `Delegate`; otherwise reports `notImplemented` - /// through the terminal error callback. + /// Invokes the app's per-state callback when the delegate conforms to `Delegate`; otherwise + /// reports `notImplemented` through the error callback. private func deliver( - _ state: MSALNativeAuthState, - internalState: MSALNativeAuthFlowInternalState, - scenario: MSALNativeAuthFlowScenario, + to delegate: MSALNativeAuthFlowDelegate, response: MSALNativeAuthFlowControllerResponse, - delegate: MSALNativeAuthFlowDelegate, as delegateType: Delegate.Type, + scenario: MSALNativeAuthFlowScenario, callback: (Delegate) async -> Void ) async { - state.internalState = internalState - state.scenario = scenario if let typedDelegate = delegate as? Delegate { await callback(typedDelegate) response.telemetryUpdate?(.success(())) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift index 1a3f2bba55..c56350eee8 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift @@ -47,7 +47,7 @@ extension MSALNativeAuthFlowScenario { /// Result produced by the unified V2 controller for a single step of a flow. enum MSALNativeAuthFlowResult { - case actionRequired(action: MSALNativeAuthAction, newState: MSALNativeAuthFlowInternalState) + case actionRequired(state: MSALNativeAuthState) case completed(MSALNativeAuthUserAccountResult) case error(error: MSALNativeAuthFlowError, newState: MSALNativeAuthFlowInternalState?) case browserRequired(url: URL, newState: MSALNativeAuthFlowInternalState) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index e3a0f49ffe..82b16c076d 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -147,6 +147,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin verifyHref: verifyHref, resendHref: response.href(for: .resend), sentTo: response.hint ?? "", + channelType: MSALNativeAuthChannelType(value: response.methodType ?? "email"), codeLength: response.codeLength ?? 0 ) } @@ -168,6 +169,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin continuationToken: continuationToken, activateHref: activateHref, sentTo: response.hint ?? "", + channelType: MSALNativeAuthChannelType(value: response.methodType ?? "email"), codeLength: response.codeLength ?? 0 ) case .collectAttributes: diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index 14c11dc840..97f9ecbd61 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -60,13 +60,13 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { /// `action == verify` on a password method: the user must enter their password. case passwordRequired(continuationToken: String, verifyHref: String) /// `action == verify`: a one-time code is required from the user. - case codeRequired(continuationToken: String, verifyHref: String, resendHref: String?, sentTo: String, codeLength: Int) + case codeRequired(continuationToken: String, verifyHref: String, resendHref: String?, sentTo: String, channelType: MSALNativeAuthChannelType, codeLength: Int) /// `action == verify` after a password, carrying a `challenge` link and the MFA methods. case mfaRequired(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod], challengeHref: String) /// `action == enroll`/`register`: strong-auth (JIT) registration is required; pick a method to enroll. case registrationRequired(continuationToken: String, enrollHref: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) /// `action == activate`: a JIT enrollment code is required from the user. - case activationRequired(continuationToken: String, activateHref: String, sentTo: String, codeLength: Int) + case activationRequired(continuationToken: String, activateHref: String, sentTo: String, channelType: MSALNativeAuthChannelType, codeLength: Int) /// `action == collectAttributes`: sign-up attributes are required from the user. case attributesRequired(continuationToken: String, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry], submitHref: String) /// `action == update`: a new password is required from the user. @@ -86,14 +86,14 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { return lToken == rToken && lHref == rHref && lHint == rHint case let (.passwordRequired(lToken, lHref), .passwordRequired(rToken, rHref)): return lToken == rToken && lHref == rHref - case let (.codeRequired(lToken, lVerify, lResend, lSent, lLen), .codeRequired(rToken, rVerify, rResend, rSent, rLen)): - return lToken == rToken && lVerify == rVerify && lResend == rResend && lSent == rSent && lLen == rLen + case let (.codeRequired(lToken, lVerify, lResend, lSent, lChannel, lLen), .codeRequired(rToken, rVerify, rResend, rSent, rChannel, rLen)): + return lToken == rToken && lVerify == rVerify && lResend == rResend && lSent == rSent && lChannel.value == rChannel.value && lLen == rLen case let (.mfaRequired(lToken, lMethods, lHref), .mfaRequired(rToken, rMethods, rHref)): return lToken == rToken && lMethods == rMethods && lHref == rHref case let (.registrationRequired(lToken, lHref, lMethods), .registrationRequired(rToken, rHref, rMethods)): return lToken == rToken && lHref == rHref && lMethods == rMethods - case let (.activationRequired(lToken, lHref, lSent, lLen), .activationRequired(rToken, rHref, rSent, rLen)): - return lToken == rToken && lHref == rHref && lSent == rSent && lLen == rLen + case let (.activationRequired(lToken, lHref, lSent, lChannel, lLen), .activationRequired(rToken, rHref, rSent, rChannel, rLen)): + return lToken == rToken && lHref == rHref && lSent == rSent && lChannel.value == rChannel.value && lLen == rLen case let (.attributesRequired(lToken, lAttrs, lHref), .attributesRequired(rToken, rAttrs, rHref)): return lToken == rToken && lAttrs == rAttrs && lHref == rHref case let (.updateRequired(lToken, lHref), .updateRequired(rToken, rHref)): diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift deleted file mode 100644 index 797d8ad0bc..0000000000 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthAction.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Actions the server can request during a Native Auth V2 (server-driven) flow. -/// -/// In V2 the server drives the flow: at each step the SDK reports the concrete -/// ``MSALNativeAuthState`` for that step through its dedicated ``MSALNativeAuthFlowDelegate`` -/// callback, and the app continues by calling the corresponding method on that state. -public enum MSALNativeAuthAction { - - /// The server requires the user to verify a one-time code. - /// Continue with ``MSALNativeAuthCodeRequiredState/submitCode(_:delegate:)``. - case codeRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) - - /// The server requires the user to enter their password. - /// Continue with ``MSALNativeAuthPasswordRequiredState/submitPassword(_:delegate:)``. - case passwordRequired - - /// The server requires the user to enter a new password (self-service password reset). - /// Continue with ``MSALNativeAuthNewPasswordRequiredState/submitNewPassword(_:delegate:)``. - case newPasswordRequired - - /// The server requires additional user attributes. - /// Continue with ``MSALNativeAuthAttributesRequiredState/submitAttributes(_:delegate:)``. - case attributesRequired(attributes: [MSALNativeAuthRequiredAttribute]) - - /// The server reports that some attributes were invalid and must be corrected. - /// Continue with ``MSALNativeAuthAttributesInvalidState/submitAttributes(_:delegate:)``. - case attributesInvalid(attributeNames: [String]) - - /// The server requires multi-factor authentication; the user must select an auth method. - /// Continue with ``MSALNativeAuthMFARequiredState/selectAuthMethod(_:verificationContact:delegate:)``. - case mfaRequired(authMethods: [MSALAuthMethod]) - - /// The server sent an MFA challenge; the user must enter the verification code. - /// Continue with ``MSALNativeAuthMFAVerificationRequiredState/submitChallenge(_:delegate:)``. - case mfaVerificationRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) - - /// The server requires strong authentication registration (JIT); the user must select an auth method. - /// Continue with ``MSALNativeAuthStrongAuthRegistrationRequiredState/selectAuthMethod(_:verificationContact:delegate:)``. - case strongAuthRegistrationRequired(authMethods: [MSALAuthMethod]) - - /// The server sent a JIT challenge; the user must enter the verification code. - /// Continue with ``MSALNativeAuthStrongAuthVerificationRequiredState/submitChallenge(_:delegate:)``. - case strongAuthVerificationRequired(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) -} diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift index 2a06d2b124..e60e74b7dc 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesInvalidState.swift @@ -34,9 +34,9 @@ public class MSALNativeAuthAttributesInvalidState: MSALNativeAuthState { /// The names of the attributes that were invalid. public let attributeNames: [String] - public init(attributeNames: [String]) { + init(internalState: MSALNativeAuthFlowInternalState, attributeNames: [String]) { self.attributeNames = attributeNames - super.init() + super.init(internalState: internalState) } /// Resubmit the corrected user attributes. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift index a4444b8f39..a7fed7da24 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthAttributesRequiredState.swift @@ -34,9 +34,9 @@ public class MSALNativeAuthAttributesRequiredState: MSALNativeAuthState { /// The attributes the server requires. public let attributes: [MSALNativeAuthRequiredAttribute] - public init(attributes: [MSALNativeAuthRequiredAttribute]) { + init(internalState: MSALNativeAuthFlowInternalState, attributes: [MSALNativeAuthRequiredAttribute]) { self.attributes = attributes - super.init() + super.init(internalState: internalState) } /// Submit user attributes. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift index b49997718c..f7396c59f6 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthCodeRequiredState.swift @@ -40,11 +40,14 @@ public class MSALNativeAuthCodeRequiredState: MSALNativeAuthState { /// The expected length of the code. public let codeLength: Int - public init(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { + init(internalState: MSALNativeAuthFlowInternalState, + sentTo: String, + channel: MSALNativeAuthChannelType, + codeLength: Int) { self.sentTo = sentTo self.channel = channel self.codeLength = codeLength - super.init() + super.init(internalState: internalState) } /// Submit a one-time verification code. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift index 377351fb2b..2869f9add3 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFARequiredState.swift @@ -34,9 +34,9 @@ public class MSALNativeAuthMFARequiredState: MSALNativeAuthState { /// The authentication methods available for selection. public let authMethods: [MSALAuthMethod] - public init(authMethods: [MSALAuthMethod]) { + init(internalState: MSALNativeAuthFlowInternalState, authMethods: [MSALAuthMethod]) { self.authMethods = authMethods - super.init() + super.init(internalState: internalState) } /// Select an authentication method for MFA. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift index d202449016..075a0180a3 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthMFAVerificationRequiredState.swift @@ -40,11 +40,11 @@ public class MSALNativeAuthMFAVerificationRequiredState: MSALNativeAuthState { /// The expected length of the code. public let codeLength: Int - public init(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { + init(internalState: MSALNativeAuthFlowInternalState, sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { self.sentTo = sentTo self.channel = channel self.codeLength = codeLength - super.init() + super.init(internalState: internalState) } /// Submit the MFA challenge response. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift index 8244a74971..ad95fa9661 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthState.swift @@ -39,35 +39,19 @@ import Foundation @objcMembers public class MSALNativeAuthState: NSObject { - /// The originating flow scenario for this state, set by the SDK when the state is created. - /// Reported alongside this state's delegate callbacks so the app can tell which flow produced - /// it. Internal detail - not part of the public API surface. - var scenario: MSALNativeAuthFlowScenario = .unknown + /// The internal state that continues the server-driven flow from this state. + let internalState: MSALNativeAuthFlowInternalState - /// The internal state that continues the server-driven flow from this state, injected by the SDK - /// when the state is created. Internal detail - not part of the public API surface. `nil` only for - /// states an app constructs directly (which cannot advance a flow). - var internalState: MSALNativeAuthFlowInternalState? + init(internalState: MSALNativeAuthFlowInternalState) { + self.internalState = internalState + super.init() + } - /// Forwards a continuation operation to the internal state. If the state has no internal state - /// (e.g. it was constructed directly by the app rather than handed back by the SDK), the delegate - /// is notified with a general error instead of silently doing nothing. + /// Forwards a continuation operation to the internal state. func run( delegate: MSALNativeAuthFlowDelegate, operation: @escaping (MSALNativeAuthFlowControlling, MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse ) { - guard let internalState = internalState else { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError( - type: .generalError, - errorDescription: "This state cannot be used to continue the flow." - ), - scenario: self.scenario - ) - } - return - } internalState.run(delegate: delegate, operation: operation) } } diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift index 81578bbdd5..8467e9dc66 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthRegistrationRequiredState.swift @@ -34,9 +34,9 @@ public class MSALNativeAuthStrongAuthRegistrationRequiredState: MSALNativeAuthSt /// The authentication methods available for registration. public let authMethods: [MSALAuthMethod] - public init(authMethods: [MSALAuthMethod]) { + init(internalState: MSALNativeAuthFlowInternalState, authMethods: [MSALAuthMethod]) { self.authMethods = authMethods - super.init() + super.init(internalState: internalState) } /// Select an authentication method for strong-auth registration. diff --git a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift index 64905c20cf..771df6feda 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthStrongAuthVerificationRequiredState.swift @@ -40,11 +40,11 @@ public class MSALNativeAuthStrongAuthVerificationRequiredState: MSALNativeAuthSt /// The expected length of the code. public let codeLength: Int - public init(sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { + init(internalState: MSALNativeAuthFlowInternalState, sentTo: String, channel: MSALNativeAuthChannelType, codeLength: Int) { self.sentTo = sentTo self.channel = channel self.codeLength = codeLength - super.init() + super.init(internalState: internalState) } /// Submit the strong-auth (JIT) challenge response. diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index d32056c803..971f06401f 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -90,16 +90,16 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { ] validatorMock.interactionResponses = [ .challengeRequired(continuationToken: "ct-2", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com"), - .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] let response = await sut.resetPassword(parameters: resetPasswordParameters()) - guard case .actionRequired(let action, _) = response.result else { + guard case .actionRequired(let state) = response.result else { return XCTFail("Expected actionRequired, got \(response.result)") } - guard case .codeRequired = action else { - return XCTFail("Expected codeRequired action, got \(action)") + guard state is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(state)") } XCTAssertTrue(requestProviderMock.authorizeChallengeStartCalled) XCTAssertTrue(requestProviderMock.resetPasswordStartCalled) @@ -146,11 +146,11 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { let response = await sut.submitCode("12345678", state: state) - guard case .actionRequired(let action, _) = response.result else { + guard case .actionRequired(let state) = response.result else { return XCTFail("Expected actionRequired, got \(response.result)") } - guard case .newPasswordRequired = action else { - return XCTFail("Expected newPasswordRequired action, got \(action)") + guard state is MSALNativeAuthNewPasswordRequiredState else { + return XCTFail("Expected newPasswordRequired state, got \(state)") } XCTAssertTrue(requestProviderMock.verifyCalled) XCTAssertEqual(requestProviderMock.verifyHrefReceived, "https://contoso.com/verify") @@ -262,17 +262,17 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_resendCode_whenCodeRequired_returnsCodeRequired() async { requestProviderMock.mockRequest() validatorMock.interactionResponses = [ - .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] let state = makeState(links: ["resend": URL(string: "https://contoso.com/resend")!]) let response = await sut.resendCode(state: state) - guard case .actionRequired(let action, _) = response.result else { + guard case .actionRequired(let state) = response.result else { return XCTFail("Expected actionRequired, got \(response.result)") } - guard case .codeRequired = action else { - return XCTFail("Expected codeRequired action, got \(action)") + guard state is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(state)") } XCTAssertTrue(requestProviderMock.challengeCalled) } @@ -303,13 +303,13 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup") ] validatorMock.interactionResponses = [ - .codeRequired(continuationToken: "ct-2", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + .codeRequired(continuationToken: "ct-2", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] let response = await sut.signUp(parameters: MSALNativeAuthSignUpParametersV2(username: "user@contoso.com")) - guard case .actionRequired(let action, _) = response.result, case .codeRequired = action else { - return XCTFail("Expected codeRequired action, got \(response.result)") + guard case .actionRequired(let state) = response.result, state is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(response.result)") } XCTAssertTrue(requestProviderMock.signUpStartCalled) } @@ -324,7 +324,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { ] validatorMock.interactionResponses = [ .attributesRequired(continuationToken: "ct-email", attributes: [emailAttribute], submitHref: "https://contoso.com/submit"), - .codeRequired(continuationToken: "ct-code", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8), + .codeRequired(continuationToken: "ct-code", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8), .attributesRequired(continuationToken: "ct-pwd", attributes: [passwordAttribute], submitHref: "https://contoso.com/submit"), .readyToComplete(continuationToken: "ct-continue") ] @@ -337,13 +337,13 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { // Step 1: sign-up start. The server asks for `email`; the SDK auto-submits it and the app // only sees the subsequent codeRequired action - never an attributesRequired for email. let startResponse = await sut.signUp(parameters: parameters) - guard case .actionRequired(let startAction, let codeState) = startResponse.result, case .codeRequired = startAction else { - return XCTFail("Expected codeRequired action, got \(startResponse.result)") + guard case .actionRequired(let codeState) = startResponse.result, codeState is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(startResponse.result)") } // Step 2: submit the email code. The server then asks for `password`; the SDK auto-submits // the originally supplied password and completes without surfacing attributesRequired. - let finalResponse = await sut.submitCode("12345678", state: codeState) + let finalResponse = await sut.submitCode("12345678", state: codeState.internalState) guard case .completed = finalResponse.result else { return XCTFail("Expected completed, got \(finalResponse.result)") } @@ -422,13 +422,13 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { ] validatorMock.interactionResponses = [ .signInMethods(continuationToken: "ct-2", methods: [emailMethod]), - .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8) + .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] let response = await sut.signIn(parameters: MSALNativeAuthSignInParameters(username: "user@contoso.com")) - guard case .actionRequired(let action, _) = response.result, case .codeRequired = action else { - return XCTFail("Expected codeRequired action, got \(response.result)") + guard case .actionRequired(let state) = response.result, state is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(response.result)") } } @@ -442,8 +442,8 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { let response = await sut.submitPassword("password", state: state) - guard case .actionRequired(let action, _) = response.result, case .mfaRequired = action else { - return XCTFail("Expected mfaRequired action, got \(response.result)") + guard case .actionRequired(let state) = response.result, state is MSALNativeAuthMFARequiredState else { + return XCTFail("Expected mfaRequired state, got \(response.result)") } XCTAssertTrue(requestProviderMock.submitPasswordCalled) } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index fd93674eed..a19d6291da 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -143,7 +143,21 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { links: ["verify": "https://contoso.com/verify", "resend": "https://contoso.com/resend"] ) let result = sut.validateInteraction(context: context, .success(response)) - XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", codeLength: 8)) + XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8)) + } + + func test_validateInteraction_verifyAction_usesServerChannelType() { + let response = makeResponse( + state: "interactionRequired", + action: "verify", + continuationToken: "ct", + codeLength: 8, + hint: "+1 (***) ***-1234", + methodType: "sms", + links: ["verify": "https://contoso.com/verify", "resend": "https://contoso.com/resend"] + ) + let result = sut.validateInteraction(context: context, .success(response)) + XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "+1 (***) ***-1234", channelType: MSALNativeAuthChannelType(value: "sms"), codeLength: 8)) } func test_validateInteraction_updateAction_returnsUpdateRequired() { From 18f46d5b7ba0714a0aabe97a4c4250303ce60ab9 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 23 Jul 2026 12:44:49 +0100 Subject: [PATCH 64/85] Thread operation apiId into V2 native-auth requests Delete the 6 network-level V2 telemetry apiIds (76001-76006) and thread each operation's apiId through the V2 request provider and controller so every request in an operation reports that operation's apiId, matching V1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- .../v2/MSALNativeAuthFlowController.swift | 106 +++++++++++++++--- .../v2/MSALNativeAuthV2RequestProvider.swift | 69 +++++++++--- ...AuthorizeChallengeContinueParameters.swift | 2 +- ...hV2AuthorizeChallengeStartParameters.swift | 2 +- .../MSALNativeAuthV2TokenParameters.swift | 2 +- .../MSALNativeAuthTelemetryApiId.swift | 7 -- .../MSALNativeAuthV2RequestProviderMock.swift | 96 +++++++++++++--- 7 files changed, 228 insertions(+), 56 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 0d8e581f68..9c38daaf3c 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -72,7 +72,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + sign_up link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, context: context) + let authorizationChallenge = await performAuthorizeChallengeStart( + flowScenario: flowScenario, + apiId: .telemetryApiIdV2SignUpStart, + context: context + ) guard case .continuationToken(let continuationToken, let signUpLink) = authorizationChallenge else { return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) } @@ -82,6 +86,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati username: parameters.username, continuationToken: continuationToken, href: signUpLink, + apiId: .telemetryApiIdV2SignUpStart, context: context ) } @@ -105,6 +110,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: flowScenario, username: parameters.username, scopes: scopes, + apiId: .telemetryApiIdV2SignUpStart, event: event, context: context, signUpAutofillValues: autofillValues @@ -122,7 +128,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + sign_in link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, context: context) + let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, apiId: apiId, context: context) guard case .continuationToken(let continuationToken, let signInLink) = authorizationChallenge else { return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) } @@ -132,6 +138,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati username: parameters.username, continuationToken: continuationToken, href: signInLink, + apiId: apiId, context: context ) } @@ -177,24 +184,26 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: flowScenario, username: parameters.username, scopes: scopes, + apiId: apiId, event: event, context: context ) } let challengeResult = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, context: context) + try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, apiId: apiId, context: context) } if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult { let verifyResult = await performInteraction(context: context) { - try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, context: context) + try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, apiId: apiId, context: context) } return await mapInteraction( verifyResult, flowScenario: flowScenario, username: parameters.username, scopes: scopes, + apiId: apiId, event: event, context: context ) @@ -205,6 +214,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: flowScenario, username: parameters.username, scopes: scopes, + apiId: apiId, event: event, context: context, fallbackHint: challengeHint @@ -218,7 +228,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let scopes = joinScopes(parameters.scopes) // Authorization challenge (expects 401 + continuation token + reset_password link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, context: context) + let authorizationChallenge = await performAuthorizeChallengeStart( + flowScenario: flowScenario, + apiId: .telemetryApiIdV2ResetPasswordStart, + context: context + ) guard case .continuationToken(let continuationToken, let resetPasswordLink) = authorizationChallenge else { return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) } @@ -228,6 +242,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati username: parameters.username, continuationToken: continuationToken, href: resetPasswordLink, + apiId: .telemetryApiIdV2ResetPasswordStart, context: context ) } @@ -237,7 +252,12 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } let challengeResult = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, context: context) + try self.requestProvider.challenge( + href: challengeHref, + continuationToken: challengeContinuationToken, + apiId: .telemetryApiIdV2ResetPasswordStart, + context: context + ) } return await mapInteraction( @@ -245,6 +265,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: flowScenario, username: parameters.username, scopes: scopes, + apiId: .telemetryApiIdV2ResetPasswordStart, event: event, context: context, fallbackHint: hint @@ -274,13 +295,20 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati : .telemetryApiIdV2SignInSubmitCode let event = makeAndStartTelemetryEvent(id: apiId, context: context) let result = await performInteraction(context: context) { - try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.verify( + href: verifyHref, + otp: code, + continuationToken: continuation.continuationToken, + apiId: apiId, + context: context + ) } return await mapInteraction( result, flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: apiId, event: event, context: context, recoverableState: state, @@ -290,13 +318,20 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati case .passwordReset: let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) let result = await performInteraction(context: context) { - try self.requestProvider.verify(href: verifyHref, otp: code, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.verify( + href: verifyHref, + otp: code, + continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, + context: context + ) } return await mapInteraction( result, flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, event: event, context: context, recoverableState: state @@ -329,6 +364,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati href: verifyHref, password: password, continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2SignInSubmitPassword, context: context ) } @@ -337,6 +373,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2SignInSubmitPassword, event: event, context: context, recoverableState: state @@ -362,6 +399,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati href: updateHref, newPassword: password, continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2ResetPasswordSubmit, context: context ) } @@ -377,7 +415,12 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } let pollResult = await performInteraction(context: context) { - try self.requestProvider.poll(href: pollHref, continuationToken: pollToken, context: context) + try self.requestProvider.poll( + href: pollHref, + continuationToken: pollToken, + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) } switch pollResult { @@ -415,6 +458,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati continuationToken: completionToken, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2ResetPasswordSubmit, event: event, context: context ) @@ -438,6 +482,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati href: submitHref, attributes: attributes, continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2SignUpSubmitAttributes, context: context ) } @@ -446,6 +491,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2SignUpSubmitAttributes, event: event, context: context, recoverableState: state, @@ -484,6 +530,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati href: enrollHref, target: verificationContact, continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2JITChallenge, context: context ) } @@ -492,6 +539,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2JITChallenge, event: event, context: context ) @@ -511,7 +559,12 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) let result = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.challenge( + href: challengeHref, + continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2MFAGetAuthMethods, + context: context + ) } return await mapInteraction( @@ -519,6 +572,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2MFAGetAuthMethods, event: event, context: context, fallbackHint: continuation.sentToHint, @@ -556,6 +610,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati href: submitHref, code: challenge, continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2MFASubmitChallenge, context: context ) } else { @@ -563,6 +618,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati href: submitHref, otp: challenge, continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2MFASubmitChallenge, context: context ) } @@ -572,6 +628,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2MFASubmitChallenge, event: event, context: context, recoverableState: state @@ -592,7 +649,12 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } let result = await performInteraction(context: context) { - try self.requestProvider.challenge(href: resendHref, continuationToken: continuation.continuationToken, context: context) + try self.requestProvider.challenge( + href: resendHref, + continuationToken: continuation.continuationToken, + apiId: .telemetryApiIdV2ResetPasswordResendCode, + context: context + ) } return await mapInteraction( @@ -600,6 +662,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, + apiId: .telemetryApiIdV2ResetPasswordResendCode, event: event, context: context, fallbackHint: continuation.sentToHint @@ -610,10 +673,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private func performAuthorizeChallengeStart( flowScenario: MSALNativeAuthFlowScenario, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { let result: Result = await send { - try self.requestProvider.authorizeChallengeStart(context: context) + try self.requestProvider.authorizeChallengeStart(apiId: apiId, context: context) } return responseValidator.validateAuthorizeChallenge(context: context, result, flowScenario: flowScenario) } @@ -621,10 +685,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private func performAuthorizeChallengeContinue( flowScenario: MSALNativeAuthFlowScenario, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { let result: Result = await send { - try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, context: context) + try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, apiId: apiId, context: context) } return responseValidator.validateAuthorizeChallenge(context: context, result, flowScenario: flowScenario) } @@ -661,6 +726,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: MSALNativeAuthFlowScenario, username: String?, scopes: [String], + apiId: MSALNativeAuthTelemetryApiId, event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, recoverableState: MSALNativeAuthFlowInternalState? = nil, @@ -676,6 +742,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati continuationToken: token, username: username, scopes: scopes, + apiId: apiId, event: event, context: context ) @@ -850,15 +917,21 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati continuationToken: String, username: String?, scopes: [String], + apiId: MSALNativeAuthTelemetryApiId, event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext ) async -> MSALNativeAuthFlowControllerResponse { - let codeResult = await performAuthorizeChallengeContinue(flowScenario: flowScenario, continuationToken: continuationToken, context: context) + let codeResult = await performAuthorizeChallengeContinue( + flowScenario: flowScenario, + continuationToken: continuationToken, + apiId: apiId, + context: context + ) guard case .authorizationCode(let code) = codeResult else { return failure(codeResult, event: event, context: context, scenario: flowScenario) } - let tokenResponseResult = await performTokenExchange(code: code, scopes: scopes, context: context) + let tokenResponseResult = await performTokenExchange(code: code, scopes: scopes, apiId: apiId, context: context) switch tokenResponseResult { case .success(let tokenResponse): do { @@ -889,11 +962,12 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private func performTokenExchange( code: String, scopes: [String], + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) async -> Result { let request: MSIDHttpRequest do { - request = try requestProvider.token(code: code, scopes: scopes, context: context) + request = try requestProvider.token(code: code, scopes: scopes, apiId: apiId, context: context) } catch { return .failure(error) } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index edcc95cdc9..51e17c9eb5 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -30,6 +30,7 @@ protocol MSALNativeAuthV2RequestProviding { func signUpStart(username: String, continuationToken: String, href: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -37,6 +38,7 @@ protocol MSALNativeAuthV2RequestProviding { func signInStart(username: String, continuationToken: String, href: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -44,6 +46,7 @@ protocol MSALNativeAuthV2RequestProviding { func resetPasswordStart(username: String, continuationToken: String, href: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -51,6 +54,7 @@ protocol MSALNativeAuthV2RequestProviding { func submitPassword(href: String, password: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -58,6 +62,7 @@ protocol MSALNativeAuthV2RequestProviding { func submitCode(href: String, code: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -65,6 +70,7 @@ protocol MSALNativeAuthV2RequestProviding { func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -72,12 +78,14 @@ protocol MSALNativeAuthV2RequestProviding { func registerMethod(href: String, target: String?, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest /// Send EOTP (server `challenge` / `resend` href). func challenge(href: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -85,6 +93,7 @@ protocol MSALNativeAuthV2RequestProviding { func verify(href: String, otp: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest @@ -92,26 +101,32 @@ protocol MSALNativeAuthV2RequestProviding { func updatePassword(href: String, newPassword: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest /// Poll for completion (server `poll` href). func poll(href: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest /// Start `authorize-challenge` (no continuation token) → `401` + continuation token. - func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest + func authorizeChallengeStart(apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest /// Continue `authorize-challenge` (with continuation token) → authorization code. func authorizeChallengeContinue(continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest /// Token exchange. func token(code: String, scopes: [String], + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest } @@ -129,12 +144,13 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func signUpStart(username: String, continuationToken: String, href: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( context: context, target: .href(href), - apiId: .telemetryApiIdV2SignUp, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, username: username, continuationToken: continuationToken @@ -144,12 +160,13 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func signInStart(username: String, continuationToken: String, href: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( context: context, target: .href(href), - apiId: .telemetryApiIdV2SignIn, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.signInStart.rawValue, username: username, continuationToken: continuationToken @@ -159,12 +176,13 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func resetPasswordStart(username: String, continuationToken: String, href: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( context: context, target: .href(href), - apiId: .telemetryApiIdV2ResetPassword, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, username: username, continuationToken: continuationToken @@ -174,13 +192,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func submitPassword(href: String, password: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.submitPassword.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, password: password) )) @@ -189,13 +208,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func submitCode(href: String, code: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.submitCode.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, code: code) )) @@ -204,13 +224,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, attributes: attributes) )) @@ -219,13 +240,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func registerMethod(href: String, target: String?, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.registerMethod.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, target: target) )) @@ -233,13 +255,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func challenge(href: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.challenge.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken) )) @@ -248,13 +271,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func verify(href: String, otp: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.verify.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, otp: otp) )) @@ -263,13 +287,14 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func updatePassword(href: String, newPassword: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "PUT", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, newPassword: newPassword) )) @@ -277,39 +302,51 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { func poll(href: String, continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2Hal, + apiId: apiId, operationType: MSALNativeAuthV2OperationType.poll.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken) )) } - func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2AuthorizeChallengeStartParameters(context: context, clientId: config.clientId)) + func authorizeChallengeStart(apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + return try configurator.configure( + parameters: MSALNativeAuthV2AuthorizeChallengeStartParameters(context: context, clientId: config.clientId, apiId: apiId) + ) } func authorizeChallengeContinue(continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure( - parameters: MSALNativeAuthV2AuthorizeChallengeContinueParameters(context: context, continuationToken: continuationToken) + parameters: MSALNativeAuthV2AuthorizeChallengeContinueParameters( + context: context, + continuationToken: continuationToken, + apiId: apiId + ) ) } func token(code: String, scopes: [String], + apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest { return try configurator.configure(parameters: MSALNativeAuthV2TokenParameters( context: context, clientId: config.clientId, code: code, - scopes: scopes + scopes: scopes, + apiId: apiId )) } } diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift index ea438a826f..74cbd126d8 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift @@ -28,8 +28,8 @@ import Foundation struct MSALNativeAuthV2AuthorizeChallengeContinueParameters: MSALNativeAuthV2Requestable { let context: MSALNativeAuthRequestContext let continuationToken: String + let apiId: MSALNativeAuthTelemetryApiId let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded - let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeContinue.rawValue var body: [AnyHashable: Any] { diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift index e28e5c0854..60f61fd128 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeStartParameters.swift @@ -28,8 +28,8 @@ import Foundation struct MSALNativeAuthV2AuthorizeChallengeStartParameters: MSALNativeAuthV2Requestable { let context: MSALNativeAuthRequestContext let clientId: String + let apiId: MSALNativeAuthTelemetryApiId let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded - let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2AuthorizeChallenge let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.authorizeChallengeStart.rawValue var body: [AnyHashable: Any] { diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift index 5e16e882ab..2801080d1c 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.swift @@ -30,8 +30,8 @@ struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { let clientId: String let code: String let scopes: [String] + let apiId: MSALNativeAuthTelemetryApiId let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded - let apiId: MSALNativeAuthTelemetryApiId = .telemetryApiIdV2Token let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.token.rawValue let expectsRawJSONResponse = true diff --git a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift index 31d70005d0..ddb2b45ab4 100644 --- a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift +++ b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift @@ -61,13 +61,6 @@ enum MSALNativeAuthTelemetryApiId: Int { case telemetryApiIdJITChallenge = 75030 case telemetryApiIdJITContinue = 75031 case telemetryApiISignInAfterJIT = 75032 - // Native Auth V2 (server-driven HAL) network requests. - case telemetryApiIdV2AuthorizeChallenge = 76001 - case telemetryApiIdV2Token = 76002 - case telemetryApiIdV2SignIn = 76003 - case telemetryApiIdV2SignUp = 76004 - case telemetryApiIdV2ResetPassword = 76005 - case telemetryApiIdV2Hal = 76006 // Native Auth V2 (server-driven HAL) controller operations. case telemetryApiIdV2SignUpStart = 76007 case telemetryApiIdV2SignInWithPasswordStart = 76008 diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index 97375c5550..4e521d2fac 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -70,17 +70,21 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { return MSALNativeAuthHTTPRequestMock.prepareMockRequest() } - func authorizeChallengeStart(context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func authorizeChallengeStart(apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { authorizeChallengeStartCalled = true return try resolveRequest() } - func authorizeChallengeContinue(continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func authorizeChallengeContinue( + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { authorizeChallengeContinueCalled = true return try resolveRequest() } - func token(code: String, scopes: [String], context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func token(code: String, scopes: [String], apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { tokenCalled = true tokenScopes = scopes if throwError { @@ -102,34 +106,70 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { "scope": "scope" ] - func resetPasswordStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func resetPasswordStart( + username: String, + continuationToken: String, + href: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { resetPasswordStartCalled = true return try resolveRequest() } - func signInStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func signInStart( + username: String, + continuationToken: String, + href: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { signInStartCalled = true return try resolveRequest() } - func signUpStart(username: String, continuationToken: String, href: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func signUpStart( + username: String, + continuationToken: String, + href: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { signUpStartCalled = true return try resolveRequest() } - func submitPassword(href: String, password: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func submitPassword( + href: String, + password: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { submitPasswordCalled = true submitPasswordHrefReceived = href return try resolveRequest() } - func submitCode(href: String, code: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func submitCode( + href: String, + code: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { submitCodeCalled = true submitCodeHrefReceived = href return try resolveRequest() } - func submitAttributes(href: String, attributes: [String: Any], continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func submitAttributes( + href: String, + attributes: [String: Any], + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { submitAttributesCalled = true submitAttributesHrefReceived = href submitAttributesReceived = attributes @@ -137,31 +177,59 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { return try resolveRequest() } - func registerMethod(href: String, target: String?, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func registerMethod( + href: String, + target: String?, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { registerMethodCalled = true registerMethodHrefReceived = href return try resolveRequest() } - func challenge(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func challenge( + href: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { challengeCalled = true challengeHrefReceived = href return try resolveRequest() } - func verify(href: String, otp: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func verify( + href: String, + otp: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { verifyCalled = true verifyHrefReceived = href return try resolveRequest() } - func updatePassword(href: String, newPassword: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func updatePassword( + href: String, + newPassword: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { updatePasswordCalled = true updateHrefReceived = href return try resolveRequest() } - func poll(href: String, continuationToken: String, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + func poll( + href: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { pollCalled = true pollHrefReceived = href return try resolveRequest() From 48a173ae7a30cf643ba80a03359ffdebb9bbd78e Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 23 Jul 2026 14:37:45 +0100 Subject: [PATCH 65/85] Add V2 native-auth network unit tests and fix stale controller test Add unit tests for the V2 (server-driven HAL) network layer mirroring the V1 test suite: HrefURLResolver URL building, the five request parameter structs, and the request provider (asserting URL, HTTP method, response serializer routing and apiId threading onto server telemetry). Register the new files in both the iOS and Mac unit-test targets. Remove obsolete validatorMock.tokenResponse assignments in MSALNativeAuthFlowControllerTests: the token response is stubbed via the request provider mock and cache accessor mock, not the validator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- MSAL/MSAL.xcodeproj/project.pbxproj | 254 ++++++++++-------- .../MSALNativeAuthFlowControllerTests.swift | 8 - ...MSALNativeAuthV2HrefURLResolverTests.swift | 112 ++++++++ .../v2/MSALNativeAuthV2ParametersTests.swift | 201 ++++++++++++++ ...MSALNativeAuthV2RequestProviderTests.swift | 236 ++++++++++++++++ 5 files changed, 685 insertions(+), 126 deletions(-) create mode 100644 MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2HrefURLResolverTests.swift create mode 100644 MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift create mode 100644 MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index a1fe4816bd..04cd6a501b 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -7,34 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 01F6FDA46510AF671264602E /* MSALNativeAuthFlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */; }; 01462653AC546A8B95A0D912 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; + 01F6FDA46510AF671264602E /* MSALNativeAuthFlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */; }; 022239DBCF2EF4AD83359DD3 /* MailTMConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */; }; - 06AAD69B63B7A013959ECEF7 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; - 1FFBE815F16A5C07BCAEEA7A /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; - 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; - 3661378FB7B5DA5CCB37D76E /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; - 42E1FE910A9592561A2F44DC /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; - 4F6C95BC33A85725CB3F2185 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; - 5A7906E804836B39F0D61EE5 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; - 5DA9B72FCAECBF4718161CE1 /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; - 6534A6BDFED26846E71370A9 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; - 6A130FEA55D11486D2F4FA55 /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; - 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; - 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; - 79B0D18719E266EBFAA96F9D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; - 969B85F56D10B314FEE5E165 /* MSALNativeAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */; }; - 9959BEB45FADB738C8BFE331 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; - B311DA009515BDDE5FDF3679 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; - B3E12C5ECC553A95521CFEFA /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; - BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; - BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; - C4675E1CCC8208251CE74818 /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; - DE5CDB156AF38066359E4B66 /* MSALNativeAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */; }; - E03A45C678944B4F5D572922 /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; - E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; - F05FC2CFEF1AE5462086AD0C /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; - F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; + 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; + 02B7A67D74D6FCC9CD5BDAFA /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; 04A6B5AE226936F30035C7C2 /* MSALFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F5BC91E59359900912CB8 /* MSALFramework.m */; }; 04A6B5AF226936F40035C7C2 /* MSALFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F5BC91E59359900912CB8 /* MSALFramework.m */; }; 04A6B5B0226936FE0035C7C2 /* MSIDVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = B2C17B091FC8DB2E0070A514 /* MSIDVersion.m */; }; @@ -108,6 +85,9 @@ 04D32CAF1FD615B3000B123E /* MSALErrorConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D32CAD1FD615B3000B123E /* MSALErrorConverter.m */; }; 04D32CD01FD8AFF3000B123E /* MSALErrorConverterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D32CCF1FD8AFF3000B123E /* MSALErrorConverterTests.m */; }; 04D32CD11FD8AFF3000B123E /* MSALErrorConverterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 04D32CCF1FD8AFF3000B123E /* MSALErrorConverterTests.m */; }; + 0529EA054FE400F5647FCBA5 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; + 06AAD69B63B7A013959ECEF7 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; + 081C1B43CDAC5F4990EA68FB /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; 0B808ECA169C3107F4335691 /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; 0D96DB3727850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */; }; 0D96DB3827850E8200DEAF87 /* MSALWipeCacheForAllAccountsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */; }; @@ -120,10 +100,8 @@ 12E2160B2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 12E2160C2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; + 192F74D7E3825C5CDCF50CEB /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */; }; 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; - F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; - D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; - 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; 1E1A2E042256D12F001009ED /* MSALTestAppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64B01E5AAC5C0086D120 /* MSALTestAppSettings.m */; }; @@ -182,6 +160,8 @@ 1EF395FF246DFAD200647FDB /* MSALAuthScheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF395FC246DFAD200647FDB /* MSALAuthScheme.m */; }; 1EF39600246DFAD200647FDB /* MSALAuthScheme.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EF395FC246DFAD200647FDB /* MSALAuthScheme.m */; }; 1EFD703424AC3E86007265FF /* MSALTestAppAsymmetricKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 1EFD703324AC3E86007265FF /* MSALTestAppAsymmetricKey.m */; }; + 1FFBE815F16A5C07BCAEEA7A /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; + 2161D7C3F3059052DD18D048 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; 23014D192567233A005E12F2 /* MSALAuthenticationSchemeProtocolInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 23014D172567233A005E12F2 /* MSALAuthenticationSchemeProtocolInternal.h */; }; 23014D1A2567233A005E12F2 /* MSALAuthenticationSchemeProtocolInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 23014D172567233A005E12F2 /* MSALAuthenticationSchemeProtocolInternal.h */; }; 23014D4525672DF9005E12F2 /* MSALAuthenticationSchemePop+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 23014D4425672DF9005E12F2 /* MSALAuthenticationSchemePop+Internal.h */; }; @@ -270,6 +250,7 @@ 23F32F0C1FF4789100B2905E /* MSIDTestURLResponse+MSAL.m in Sources */ = {isa = PBXBuildFile; fileRef = 23F32F061FF4787600B2905E /* MSIDTestURLResponse+MSAL.m */; }; 23F32F0D1FF4789200B2905E /* MSIDTestURLResponse+MSAL.m in Sources */ = {isa = PBXBuildFile; fileRef = 23F32F061FF4787600B2905E /* MSIDTestURLResponse+MSAL.m */; }; 23FB5C1E22542B99002BF1EB /* MSALJsonDeserializable.h in Headers */ = {isa = PBXBuildFile; fileRef = 23FB5C1C22542B99002BF1EB /* MSALJsonDeserializable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; 280095EB2C32CAFC00F1653E /* ClientIdType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 280095EA2C32CAFC00F1653E /* ClientIdType.swift */; }; 2809E8352C3C37B7009F14D7 /* MSALNativeAuthEndToEndPasswordTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2809E8342C3C37B7009F14D7 /* MSALNativeAuthEndToEndPasswordTestCase.swift */; }; 28188F622C8F48BD00CFDD05 /* MSALNativeAuthSignInWithMFAEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28188F5F2C8F482D00CFDD05 /* MSALNativeAuthSignInWithMFAEndToEndTests.swift */; }; @@ -424,21 +405,27 @@ 28FDC4A92A38C0D100E38BE1 /* SignInAfterSignUpError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDC4A82A38C0D000E38BE1 /* SignInAfterSignUpError.swift */; }; 28FDC4AE2A38D81100E38BE1 /* MSALNativeAuthSignInControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDC4AB2A38D7D200E38BE1 /* MSALNativeAuthSignInControllerMock.swift */; }; 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; + 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; + 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; - 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; - 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; - 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; + 31A0E8B0B69F886271896E3E /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; + 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; - 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; - 0529EA054FE400F5647FCBA5 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; - D5449AE1C2AE8608DA837967 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; - 31A0E8B0B69F886271896E3E /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; + 3661378FB7B5DA5CCB37D76E /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; + 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; 38880DF423280C5900688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 38880DF523280C5A00688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; + 3F2E65884A64B912E42B512D /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; + 42E1FE910A9592561A2F44DC /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; + 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; + 4CEDE2C62AFBCC69A07C8652 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; + 4F6C95BC33A85725CB3F2185 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; + 547D9B6A1EA16110560F531F /* MSALNativeAuthV2ParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */; }; 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; @@ -448,7 +435,10 @@ 58B81F7124AC5D7200E8799E /* MSALTestCacheTokenResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */; }; 58B81F7224AC5D7300E8799E /* MSALTestCacheTokenResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */; }; 58BBA11E25C1406F007B3EF6 /* MSAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; }; + 5A7906E804836B39F0D61EE5 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; + 5DA9B72FCAECBF4718161CE1 /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; 5E471E84AA33CFA840BBA964 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; + 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; 6077D4A022498BFF001798A2 /* MSALTenantProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 6077D49F22498BFF001798A2 /* MSALTenantProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6077D4A122498BFF001798A2 /* MSALTenantProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 6077D49F22498BFF001798A2 /* MSALTenantProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6077D4A922498D87001798A2 /* MSALTenantProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 6077D4A822498D87001798A2 /* MSALTenantProfile.m */; }; @@ -456,10 +446,14 @@ 609AF9332256BD0C00E2978D /* MSALAccountsProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609AF9322256BD0C00E2978D /* MSALAccountsProviderTests.m */; }; 64463489E8DC5172D49F98FF /* MailTMHTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F1413DA1D76D5EF31F4EC /* MailTMHTTPClient.swift */; }; 6525115A29CD84A000D3B876 /* MSALPublicClientApplicationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D673F07C1E4AAB0D0018BA91 /* MSALPublicClientApplicationTests.m */; }; + 6534A6BDFED26846E71370A9 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; + 6A130FEA55D11486D2F4FA55 /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; + 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; - 2161D7C3F3059052DD18D048 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; + 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; + 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; 7207E63A2FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -485,10 +479,16 @@ 7248CF9C2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 7248CF9D2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; 7248CF9E2F9AF2F90038E238 /* MSALDeviceTokenResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */; }; + 7662552749019C91197EA86B /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; + 76ACA3209E92AFC0CD4988B0 /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; 76EEE63606562E71DCFDA606 /* MSALNativeAuthFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */; }; + 79B0D18719E266EBFAA96F9D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; + 7B9A32EA8EE3F6A20D0CFA80 /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; + 827CE360F94F0A5BCA875193 /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */; }; 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; + 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; 886F515829CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; 886F515929CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; 886F515A29CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; }; @@ -505,9 +505,6 @@ 8DDF473F2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDF473E2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift */; }; 8E0486CA55F25C1987E4067A /* MSALNativeAuthFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */; }; 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; - C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; - 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; - CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; 91AA24592BDF643A005037EA /* MSAL_Test_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA24582BDF643A005037EA /* MSAL_Test_App.swift */; }; 91AA245B2BDF643A005037EA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA245A2BDF643A005037EA /* ContentView.swift */; }; 91AA245D2BDF6440005037EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 91AA245C2BDF6440005037EA /* Assets.xcassets */; }; @@ -547,6 +544,7 @@ 96902DF920E157B400200E6F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DF520E1579000200E6F /* WebKit.framework */; }; 96902DFB20E158E700200E6F /* GSS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DFA20E158E700200E6F /* GSS.framework */; }; 96902DFD20E1590200200E6F /* SecurityInterface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DFC20E1590200200E6F /* SecurityInterface.framework */; }; + 969B85F56D10B314FEE5E165 /* MSALNativeAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */; }; 96B5E6CF2256D152002232F9 /* MSALCacheConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 96B5E6CC2256D152002232F9 /* MSALCacheConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96B5E6D02256D152002232F9 /* MSALCacheConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 96B5E6CD2256D152002232F9 /* MSALCacheConfig.m */; }; 96B5E6D12256D152002232F9 /* MSALCacheConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 96B5E6CD2256D152002232F9 /* MSALCacheConfig.m */; }; @@ -595,6 +593,7 @@ 96CF95312268FD0500D97374 /* MSALJsonSerializable.h in Headers */ = {isa = PBXBuildFile; fileRef = 232D616922498EDF00260C42 /* MSALJsonSerializable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96CF95322268FD0500D97374 /* MSALJsonDeserializable.h in Headers */ = {isa = PBXBuildFile; fileRef = 23FB5C1C22542B99002BF1EB /* MSALJsonDeserializable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96CFA00B1E6E3460003BFCDC /* MSALTestAppScopesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 96CFA0091E6E3454003BFCDC /* MSALTestAppScopesViewController.m */; }; + 9959BEB45FADB738C8BFE331 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; 9B235D9D2A3CC71C00657331 /* NativeAuthEndToEndTestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 9B235D952A3CC71C00657331 /* NativeAuthEndToEndTestPlan.xctestplan */; }; 9B2BBA2F2A3293330075F702 /* MSALNativeAuthResetPasswordStartValidatedErrorTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2BBA2D2A3292400075F702 /* MSALNativeAuthResetPasswordStartValidatedErrorTypeTests.swift */; }; 9B2BBA312A3296010075F702 /* MSALNativeAuthResetPasswordChallengeResponseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B2BBA302A3296010075F702 /* MSALNativeAuthResetPasswordChallengeResponseErrorTests.swift */; }; @@ -634,9 +633,11 @@ A09AAFC324C00B3600C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A09AAFC424C00B3700C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A370E8BCE6A05E05ECC63027 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; + A4B46554DA558BD5457129CF /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; + AA5AB06A9DD86202FD19BFC8 /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; AE64B3751432B2A8DD6C7FAB /* MailTMConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */; }; B203459521AF77FB00B221AA /* MSALRedirectUri.h in Headers */ = {isa = PBXBuildFile; fileRef = B203459221AF77FB00B221AA /* MSALRedirectUri.h */; settings = {ATTRIBUTES = (Public, ); }; }; B203459621AF77FB00B221AA /* MSALRedirectUri.m in Sources */ = {isa = PBXBuildFile; fileRef = B203459321AF77FB00B221AA /* MSALRedirectUri.m */; }; @@ -1036,26 +1037,25 @@ B2FBB3DA28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FBB3DB28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FE601B20E5BB5800502BA6 /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B311DA009515BDDE5FDF3679 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + B3E12C5ECC553A95521CFEFA /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; + B4CDF4FB20138CF27310258B /* MSALNativeAuthV2RequestProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */; }; + B5A1E2121EE36D3BC037113D /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; + BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; + C34EB4B71143A1078F0B72E2 /* MSALNativeAuthV2ParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */; }; + C4675E1CCC8208251CE74818 /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; + C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; + C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; - 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; + CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; + D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; - 081C1B43CDAC5F4990EA68FB /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; - 3F2E65884A64B912E42B512D /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; - 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; - 02B7A67D74D6FCC9CD5BDAFA /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; - 76ACA3209E92AFC0CD4988B0 /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; - 7B9A32EA8EE3F6A20D0CFA80 /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; - 7662552749019C91197EA86B /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; - B5A1E2121EE36D3BC037113D /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; - AA5AB06A9DD86202FD19BFC8 /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; - 4CEDE2C62AFBCC69A07C8652 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; - A4B46554DA558BD5457129CF /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; - 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; - C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; - 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; + D3C4A02BF6F6E02B8D58ACE8 /* MSALNativeAuthV2RequestProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */; }; + D5449AE1C2AE8608DA837967 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; D61A64941E5AA7D60086D120 /* MSALTestAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64801E5AA7C60086D120 /* MSALTestAppDelegate.m */; }; D61A64951E5AA7D60086D120 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64811E5AA7C60086D120 /* main.m */; }; D61A64A91E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A649D1E5AABC50086D120 /* MSALTestAppAcquireTokenViewController.m */; }; @@ -1193,18 +1193,14 @@ DE43150A2D3E551F009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */; }; DE43150B2D3E551F009A7FA2 /* MSALNativeAuthSignInParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */; }; DE43150C2D3E551F009A7FA2 /* MSALNativeAuthSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */; }; - FE0A0B00000000000000A002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */; }; DE43150D2D3E551F009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */; }; DE43150E2D3E551F009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */; }; - FE0A0B00000000000000A001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; DE43150F2D3E551F009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */; }; DE4315102D3E551F009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */; }; DE4315112D3E551F009A7FA2 /* MSALNativeAuthSignInParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */; }; DE4315122D3E551F009A7FA2 /* MSALNativeAuthSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */; }; - FE0A0B00000000000000B002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */; }; DE4315132D3E551F009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */; }; DE4315142D3E551F009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */; }; - FE0A0B00000000000000B001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; DE4315152D3E551F009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */; }; DE4F0F3129D6F1AA00D561FD /* MSALNativeAuthTokenIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4F0F2929D6F1AA00D561FD /* MSALNativeAuthTokenIntegrationTests.swift */; }; DE54B5912A434B9B00460B34 /* MSALNativeAuthTokenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */; }; @@ -1228,6 +1224,7 @@ DE5738BC2A8F79A800D9120D /* MSALNativeAuthResetPasswordPollCompletionResponseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5738BB2A8F79A800D9120D /* MSALNativeAuthResetPasswordPollCompletionResponseErrorTests.swift */; }; DE5738BE2A8F7AC600D9120D /* MSALNativeAuthResetPasswordStartOauth2ErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5738BD2A8F7AC600D9120D /* MSALNativeAuthResetPasswordStartOauth2ErrorCodeTests.swift */; }; DE5738C02A8F7C2000D9120D /* MSALNativeAuthSignUpStartResponseErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5738BF2A8F7C1F00D9120D /* MSALNativeAuthSignUpStartResponseErrorTests.swift */; }; + DE5CDB156AF38066359E4B66 /* MSALNativeAuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */; }; DE6BF3242C418C8A000BB2D9 /* MSALNativeAuthEndToEndPasswordTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2809E8342C3C37B7009F14D7 /* MSALNativeAuthEndToEndPasswordTestCase.swift */; }; DE6BF32D2C419325000BB2D9 /* libIdentityAutomationTestLib Mac.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B21FA9BE2204DC5700806B68 /* libIdentityAutomationTestLib Mac.a */; }; DE729ECD2A1793A100A761D9 /* MSALNativeAuthChannelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE729ECC2A1793A100A761D9 /* MSALNativeAuthChannelType.swift */; }; @@ -1644,6 +1641,7 @@ DEFE87722CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; DEFE87732CA6BC91009D11DC /* CredentialsDelegateSpies.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876E2CA6BC91009D11DC /* CredentialsDelegateSpies.swift */; }; DEFE87742CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFE876F2CA6BC91009D11DC /* MSALNativeAuthUserAccountEndToEndTests.swift */; }; + E03A45C678944B4F5D572922 /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; E04298BA8ED8FBE431F561A2 /* MSALNativeAuthFlowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */; }; E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */; }; E2025CC92B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025CC82B2A182200E32871 /* MSALNativeAuthSubErrorCode.swift */; }; @@ -1775,10 +1773,18 @@ E2F890052B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */; }; E2F8900E2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */; }; E68C311BD4DDECABFAA212FD /* MSALNativeAuthFlowControlling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */; }; + E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; + F05FC2CFEF1AE5462086AD0C /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; + F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; + F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; FADE0000000000000000AA03 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; + FE0A0B00000000000000A001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; + FE0A0B00000000000000A002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */; }; + FE0A0B00000000000000B001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; + FE0A0B00000000000000B002 /* MSALNativeAuthSignUpParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -2132,9 +2138,12 @@ 04D32CAC1FD61585000B123E /* MSALErrorConverter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALErrorConverter.h; sourceTree = ""; }; 04D32CAD1FD615B3000B123E /* MSALErrorConverter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALErrorConverter.m; sourceTree = ""; }; 04D32CCF1FD8AFF3000B123E /* MSALErrorConverterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALErrorConverterTests.m; sourceTree = ""; }; + 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2LinkRelation.swift; sourceTree = ""; }; 0D96DB2E27850E1300DEAF87 /* MSALWipeCacheForAllAccountsConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALWipeCacheForAllAccountsConfig.h; sourceTree = ""; }; 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWipeCacheForAllAccountsConfig.m; sourceTree = ""; }; 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorityURLFormat.swift; sourceTree = ""; }; + 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolverTests.swift; sourceTree = ""; }; + 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBody.swift; sourceTree = ""; }; 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ValidatedResponses.swift; sourceTree = ""; }; 1E04571F24BD5A7D00444756 /* MSALCacheItemDetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCacheItemDetailViewController.h; sourceTree = ""; }; 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCacheItemDetailViewController.m; sourceTree = ""; }; @@ -2220,6 +2229,7 @@ 23F32F051FF4787600B2905E /* MSIDTestURLResponse+MSAL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSIDTestURLResponse+MSAL.h"; sourceTree = ""; }; 23F32F061FF4787600B2905E /* MSIDTestURLResponse+MSAL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSIDTestURLResponse+MSAL.m"; sourceTree = ""; }; 23FB5C1C22542B99002BF1EB /* MSALJsonDeserializable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALJsonDeserializable.h; sourceTree = ""; }; + 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2EntryParameters.swift; sourceTree = ""; }; 280095EA2C32CAFC00F1653E /* ClientIdType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIdType.swift; sourceTree = ""; }; 2809E8342C3C37B7009F14D7 /* MSALNativeAuthEndToEndPasswordTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthEndToEndPasswordTestCase.swift; sourceTree = ""; }; 28188F5F2C8F482D00CFDD05 /* MSALNativeAuthSignInWithMFAEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInWithMFAEndToEndTests.swift; sourceTree = ""; }; @@ -2319,26 +2329,12 @@ 28FDC4AB2A38D7D200E38BE1 /* MSALNativeAuthSignInControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInControllerMock.swift; sourceTree = ""; }; 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseErrorHandler.swift; sourceTree = ""; }; 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenRequestHandling.swift; sourceTree = ""; }; - 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; - 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; - 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2LinkRelation.swift; sourceTree = ""; }; - 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBodyKey.swift; sourceTree = ""; }; - 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBody.swift; sourceTree = ""; }; 475F1413DA1D76D5EF31F4EC /* MailTMHTTPClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailTMHTTPClient.swift; sourceTree = ""; }; 49AAD919E560052DA700D2DA /* RetryExecutor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RetryExecutor.swift; sourceTree = ""; }; + 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthRegistrationRequiredState.swift; sourceTree = ""; }; - 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFAVerificationRequiredState.swift; sourceTree = ""; }; - 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthNewPasswordRequiredState.swift; sourceTree = ""; }; - 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowError.swift; sourceTree = ""; }; - 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowScenario.swift; sourceTree = ""; }; - 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthCodeRequiredState.swift; sourceTree = ""; }; - 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFARequiredState.swift; sourceTree = ""; }; - A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesRequiredState.swift; sourceTree = ""; }; - B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthPasswordRequiredState.swift; sourceTree = ""; }; - B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowDelegate.swift; sourceTree = ""; }; - BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesInvalidState.swift; sourceTree = ""; }; - FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthVerificationRequiredState.swift; sourceTree = ""; }; - FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthState.swift; sourceTree = ""; }; + 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerResponse.swift; sourceTree = ""; }; + 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 58B81F6524AC59A000E8799E /* MSALTestCacheTokenResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTestCacheTokenResponse.h; sourceTree = ""; }; 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTestCacheTokenResponse.m; sourceTree = ""; }; @@ -2348,7 +2344,9 @@ 609AF958225B348900E2978D /* MSALTenantProfile+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTenantProfile+Internal.h"; sourceTree = ""; }; 60DEF15A1E67756800966664 /* MSAL Test App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = "MSAL Test App.entitlements"; path = "../../../../MSAL Test App.entitlements"; sourceTree = ""; }; 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowInternalState.swift; sourceTree = ""; }; + 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFAVerificationRequiredState.swift; sourceTree = ""; }; 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerMock.swift; sourceTree = ""; }; + 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALAction.swift; sourceTree = ""; }; 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidator.swift; sourceTree = ""; }; 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALDeviceTokenResult+Internal.h"; sourceTree = ""; }; 7207E63E2FA97BBC008F6803 /* MSALDeviceTokenParametersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenParametersTests.m; sourceTree = ""; }; @@ -2359,17 +2357,22 @@ 7248CF9A2F9AF2F80038E238 /* MSALDeviceTokenResult.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenResult.m; sourceTree = ""; }; 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerTests.swift; sourceTree = ""; }; 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowResult.swift; sourceTree = ""; }; - B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowContinuationState.swift; sourceTree = ""; }; - 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerResponse.swift; sourceTree = ""; }; - 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowResponseDispatcher.swift; sourceTree = ""; }; + 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthNewPasswordRequiredState.swift; sourceTree = ""; }; + 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2TokenParameters.swift; sourceTree = ""; }; + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowError.swift; sourceTree = ""; }; + 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowScenario.swift; sourceTree = ""; }; 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCIAMAuthority.h; sourceTree = ""; }; 886F516329CCA58900F09471 /* MSALCIAMAuthority.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthority.m; sourceTree = ""; }; 88A25ED229E7185B00066311 /* MSALCIAMAuthorityTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCIAMAuthorityTests.m; sourceTree = ""; }; + 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBodyKey.swift; sourceTree = ""; }; + 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowResponseDispatcher.swift; sourceTree = ""; }; 8D2733132AD8346D00AD67FD /* MSALNativeAuthCustomErrorSerializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthCustomErrorSerializer.swift; sourceTree = ""; }; 8D35C8E62A97BD0000BEC29A /* MSALNativeAuthErrorBasicAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthErrorBasicAttribute.swift; sourceTree = ""; }; 8D35C8F02A97BD2300BEC29A /* MSALNativeAuthRequiredAttributeOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequiredAttributeOptions.swift; sourceTree = ""; }; 8D61F9A02A66AC9D00468E18 /* MSALNativeAuthRequestableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequestableTests.swift; sourceTree = ""; }; 8DDF473E2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequiredAttribute.swift; sourceTree = ""; }; + 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthCodeRequiredState.swift; sourceTree = ""; }; + 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFARequiredState.swift; sourceTree = ""; }; 91AA24522BDF6439005037EA /* MSAL Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MSAL Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 91AA24582BDF643A005037EA /* MSAL_Test_App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSAL_Test_App.swift; sourceTree = ""; }; 91AA245A2BDF643A005037EA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -2381,6 +2384,7 @@ 91AA24822BDF6DDE005037EA /* MSAL Test App (visionOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "MSAL Test App (visionOS).entitlements"; sourceTree = ""; }; 91AA248C2BDF72FC005037EA /* MSAL_Test_App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSAL_Test_App-Bridging-Header.h"; sourceTree = ""; }; 91AA248D2BDF7A41005037EA /* msal__test_app__vision.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = msal__test_app__vision.xcconfig; sourceTree = ""; }; + 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderTests.swift; sourceTree = ""; }; 94E876B01E4556B400FB96ED /* MSAL.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSAL.pch; sourceTree = ""; }; 94E876CA1E492D6000FB96ED /* MSALAuthority.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSALAuthority.h; sourceTree = ""; }; 94E876CB1E492D6000FB96ED /* MSALAuthority.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSALAuthority.m; sourceTree = ""; }; @@ -2405,7 +2409,6 @@ 963377BE211E14C600943EE0 /* MSALWebviewType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWebviewType.m; sourceTree = ""; }; 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALResponse.swift; sourceTree = ""; }; - 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALAction.swift; sourceTree = ""; }; 9648AF54225D826500F66801 /* MSALTelemetryConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTelemetryConfig+Internal.h"; sourceTree = ""; }; 9648AF5B225DD6A900F66801 /* MSALGlobalConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALGlobalConfig+Internal.h"; sourceTree = ""; }; 9682A62A218290F700E37E63 /* MSALDefinitions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDefinitions.h; sourceTree = ""; }; @@ -2431,6 +2434,7 @@ 96B5E6F12256D197002232F9 /* MSALExtraQueryParameters.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALExtraQueryParameters.m; sourceTree = ""; }; 96CFA0081E6E3454003BFCDC /* MSALTestAppScopesViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSALTestAppScopesViewController.h; sourceTree = ""; }; 96CFA0091E6E3454003BFCDC /* MSALTestAppScopesViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSALTestAppScopesViewController.m; sourceTree = ""; }; + 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Requestable.swift; sourceTree = ""; }; 9B235D952A3CC71C00657331 /* NativeAuthEndToEndTestPlan.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = NativeAuthEndToEndTestPlan.xctestplan; sourceTree = ""; }; 9B235D9E2A3CFB4300657331 /* MSALNativeAuthEndToEndBaseTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthEndToEndBaseTestCase.swift; sourceTree = ""; }; 9B235DA02A3CFC4500657331 /* MSALNativeAuthSignInUsernameEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInUsernameEndToEndTests.swift; sourceTree = ""; }; @@ -2442,6 +2446,7 @@ 9B2E93442A0D3801008A5DD2 /* MSALNativeAuthResetPasswordControlling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordControlling.swift; sourceTree = ""; }; 9B4EE9CD2A1686A900F243C1 /* MSALNativeAuthResetPasswordControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordControllerTests.swift; sourceTree = ""; }; 9B4EE9D62A16874F00F243C1 /* MSALNativeAuthResetPasswordResponseValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordResponseValidator.swift; sourceTree = ""; }; + 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2AuthorizeChallengeStartParameters.swift; sourceTree = ""; }; 9B5D6D052A3CA0E300521576 /* MSALNativeAuthSignInUsernameAndPasswordEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInUsernameAndPasswordEndToEndTests.swift; sourceTree = ""; }; 9B5D6D072A3CA55600521576 /* SignInDelegateSpies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInDelegateSpies.swift; sourceTree = ""; }; 9B61C9122A27E51900CE9E3A /* MSALNativeAuthResetPasswordRequestProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordRequestProviderMock.swift; sourceTree = ""; }; @@ -2463,6 +2468,7 @@ A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAuthSchemeTests.m; sourceTree = ""; }; A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDevicePopManagerUtil.m; sourceTree = ""; }; A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; + A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesRequiredState.swift; sourceTree = ""; }; B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProvider.swift; sourceTree = ""; }; B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenCacher.swift; sourceTree = ""; }; B203459221AF77FB00B221AA /* MSALRedirectUri.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALRedirectUri.h; sourceTree = ""; }; @@ -2621,19 +2627,19 @@ B2F45744211E41C100818910 /* MSALB2CInteractiveTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALB2CInteractiveTests.m; sourceTree = ""; }; B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALWPJMetaData+Internal.h"; sourceTree = ""; }; B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControlling.swift; sourceTree = ""; }; + B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthPasswordRequiredState.swift; sourceTree = ""; }; + B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowContinuationState.swift; sourceTree = ""; }; + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowDelegate.swift; sourceTree = ""; }; B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRequestInterceptorBridge.swift; sourceTree = ""; }; + BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesInvalidState.swift; sourceTree = ""; }; C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowController.swift; sourceTree = ""; }; C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorTests.swift; sourceTree = ""; }; - D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestConfigurator.swift; sourceTree = ""; }; - 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Requestable.swift; sourceTree = ""; }; + C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ParametersTests.swift; sourceTree = ""; }; + C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailTMConstants.swift; sourceTree = ""; }; CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestTarget.swift; sourceTree = ""; }; - 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2AuthorizeChallengeStartParameters.swift; sourceTree = ""; }; + D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestConfigurator.swift; sourceTree = ""; }; D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift; sourceTree = ""; }; - 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2TokenParameters.swift; sourceTree = ""; }; - 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2EntryParameters.swift; sourceTree = ""; }; - EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefParameters.swift; sourceTree = ""; }; - C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailTMConstants.swift; sourceTree = ""; }; D61A63F11E5979200086D120 /* MSALResult+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSALResult+Internal.h"; sourceTree = ""; }; D61A64331E5A29580086D120 /* MSAL Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MSAL Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; D61A64661E5AA6B40086D120 /* msal__test_app__ios.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = msal__test_app__ios.xcconfig; sourceTree = ""; }; @@ -2759,12 +2765,10 @@ DE40A4D22A8F80C100928CEE /* MSALNativeAuthSignUpContinueResponseErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpContinueResponseErrorTests.swift; sourceTree = ""; }; DE4315032D3E551E009A7FA2 /* MSALNativeAuthGetAccessTokenParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthGetAccessTokenParameters.swift; sourceTree = ""; }; DE4315042D3E551E009A7FA2 /* MSALNativeAuthResetPasswordParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordParameters.swift; sourceTree = ""; }; - FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordParametersV2.swift; sourceTree = ""; }; DE4315052D3E551E009A7FA2 /* MSALNativeAuthSignInAfterResetPasswordParameters .swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MSALNativeAuthSignInAfterResetPasswordParameters .swift"; sourceTree = ""; }; DE4315062D3E551E009A7FA2 /* MSALNativeAuthSignInAfterSignUpParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInAfterSignUpParameters.swift; sourceTree = ""; }; DE4315072D3E551E009A7FA2 /* MSALNativeAuthSignInParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInParameters.swift; sourceTree = ""; }; DE4315082D3E551E009A7FA2 /* MSALNativeAuthSignUpParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpParameters.swift; sourceTree = ""; }; - FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpParametersV2.swift; sourceTree = ""; }; DE4F0F2929D6F1AA00D561FD /* MSALNativeAuthTokenIntegrationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenIntegrationTests.swift; sourceTree = ""; }; DE53C7D4293F9F5A00E5B2BB /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenController.swift; sourceTree = ""; }; @@ -3018,8 +3022,13 @@ E2F626B22A781CE300C4A303 /* SignInDelegatesSpies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInDelegatesSpies.swift; sourceTree = ""; }; E2F890042B755355001FBC7C /* MSALNativeAuthUnknownCaseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUnknownCaseProtocol.swift; sourceTree = ""; }; E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthUnknownCaseProtocolTests.swift; sourceTree = ""; }; + EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefParameters.swift; sourceTree = ""; }; F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolver.swift; sourceTree = ""; }; FADE0000000000000000AA01 /* HALResource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HALResource.swift; sourceTree = ""; }; + FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthVerificationRequiredState.swift; sourceTree = ""; }; + FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordParametersV2.swift; sourceTree = ""; }; + FE0A0B00000000000000F002 /* MSALNativeAuthSignUpParametersV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignUpParametersV2.swift; sourceTree = ""; }; + FF41A7FB39DDF09BF8D97B35 /* MSALNativeAuthState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthState.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -3663,6 +3672,32 @@ path = v2; sourceTree = ""; }; + 4E0AA7DA6DFC949218EEA54E /* v2 */ = { + isa = PBXGroup; + children = ( + C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */, + 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */, + C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */, + 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 5731594DCD4698A57CAB2D10 /* parameters */ = { + isa = PBXGroup; + children = ( + 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */, + CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */, + 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */, + D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */, + 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */, + 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */, + EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */, + ); + path = parameters; + sourceTree = ""; + }; 58F36BCB7532CC3D9D60EE3A /* state */ = { isa = PBXGroup; children = ( @@ -3680,26 +3715,6 @@ path = state; sourceTree = ""; }; - F58967D39C050697CB65B47C /* v2 */ = { - isa = PBXGroup; - children = ( - 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, - B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, - 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, - 58F36BCB7532CC3D9D60EE3A /* state */, - ); - path = v2; - sourceTree = ""; - }; - 4E0AA7DA6DFC949218EEA54E /* v2 */ = { - isa = PBXGroup; - children = ( - C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */, - ); - name = v2; - path = v2; - sourceTree = ""; - }; 91AA245E2BDF6441005037EA /* Preview Content */ = { isa = PBXGroup; children = ( @@ -5580,18 +5595,15 @@ path = v2; sourceTree = ""; }; - 5731594DCD4698A57CAB2D10 /* parameters */ = { + F58967D39C050697CB65B47C /* v2 */ = { isa = PBXGroup; children = ( - 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */, - CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */, - 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */, - D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */, - 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */, - 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */, - EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */, + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, + 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, + 58F36BCB7532CC3D9D60EE3A /* state */, ); - path = parameters; + path = v2; sourceTree = ""; }; /* End PBXGroup section */ @@ -8040,6 +8052,9 @@ D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, E04298BA8ED8FBE431F561A2 /* MSALNativeAuthFlowControllerTests.swift in Sources */, A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, + 192F74D7E3825C5CDCF50CEB /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */, + 547D9B6A1EA16110560F531F /* MSALNativeAuthV2ParametersTests.swift in Sources */, + B4CDF4FB20138CF27310258B /* MSALNativeAuthV2RequestProviderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8223,6 +8238,9 @@ F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, 9D57981C31A9157AF52A29B9 /* MSALNativeAuthFlowControllerTests.swift in Sources */, A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, + 827CE360F94F0A5BCA875193 /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */, + C34EB4B71143A1078F0B72E2 /* MSALNativeAuthV2ParametersTests.swift in Sources */, + D3C4A02BF6F6E02B8D58ACE8 /* MSALNativeAuthV2RequestProviderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 971f06401f..19c99c5203 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -188,7 +188,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { requestProviderMock.mockRequest() validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/verify")!]) @@ -206,7 +205,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { requestProviderMock.mockRequest() validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signUp, links: ["verify": URL(string: "https://contoso.com/verify")!]) @@ -231,7 +229,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { validatorMock.authorizeChallengeResponses = [ .authorizationCode(code: "auth-code") ] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(links: ["update": URL(string: "https://contoso.com/update")!]) @@ -328,7 +325,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .attributesRequired(continuationToken: "ct-pwd", attributes: [passwordAttribute], submitHref: "https://contoso.com/submit"), .readyToComplete(continuationToken: "ct-continue") ] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let parameters = MSALNativeAuthSignUpParametersV2(username: "user@contoso.com") @@ -398,7 +394,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .passwordRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/pw/verify"), .readyToComplete(continuationToken: "ct-4") ] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let params = MSALNativeAuthSignInParameters(username: "user@contoso.com") @@ -452,7 +447,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { requestProviderMock.mockRequest() validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signUp, links: ["submitAttributes": URL(string: "https://contoso.com/submitattributes")!]) @@ -468,7 +462,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { requestProviderMock.mockRequest() validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/mfa/verify")!]) @@ -487,7 +480,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { requestProviderMock.mockRequest() validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - validatorMock.tokenResponse = .success(accessToken: "access-token") cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() let state = makeState(flowScenario: .signIn, links: ["activate": URL(string: "https://contoso.com/jit/activate")!]) diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2HrefURLResolverTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2HrefURLResolverTests.swift new file mode 100644 index 0000000000..40bb562b21 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2HrefURLResolverTests.swift @@ -0,0 +1,112 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL + +final class MSALNativeAuthV2HrefURLResolverTests: XCTestCase { + + private let authorityURL = URL(string: "https://login.microsoftonline.com/common")! + + private func resolver(dataCenter: String? = nil) -> MSALNativeAuthV2HrefURLResolver { + return MSALNativeAuthV2HrefURLResolver(authorityURL: authorityURL, dataCenter: dataCenter) + } + + // MARK: - Fixed endpoints + + func test_url_forAuthorizeChallengeEndpoint_appendsPathToAuthority() throws { + let url = try resolver().url(for: .authorizeChallenge) + XCTAssertEqual(url.absoluteString, "https://login.microsoftonline.com/common/oauth2/v2.0/authorize-challenge") + } + + func test_url_forTokenEndpoint_appendsPathToAuthority() throws { + let url = try resolver().url(for: .token) + XCTAssertEqual(url.absoluteString, "https://login.microsoftonline.com/common/oauth2/v2.0/token") + } + + func test_url_forEndpoint_whenDataCenterSet_appendsDcQueryItem() throws { + let url = try resolver(dataCenter: "ESTS-PUB-TEST").url(for: .token) + XCTAssertEqual(url.absoluteString, "https://login.microsoftonline.com/common/oauth2/v2.0/token?dc=ESTS-PUB-TEST") + } + + // MARK: - Absolute hrefs + + func test_url_forAbsoluteHref_isUsedAsIs() throws { + let href = "https://contoso.example.com/foo/bar?x=1" + let url = try resolver().url(forHref: href) + XCTAssertEqual(url.absoluteString, href) + } + + func test_url_forAbsoluteHref_whenDataCenterSet_appendsDc() throws { + let url = try resolver(dataCenter: "ESTS-DC").url(forHref: "https://contoso.example.com/foo") + XCTAssertEqual(url.absoluteString, "https://contoso.example.com/foo?dc=ESTS-DC") + } + + // MARK: - Relative / templated hrefs + + func test_url_forTemplatedTenantHref_stripsTenantAndAnchorsOnAuthorityTenant() throws { + let href = "{tenant}/api/v0.1/auth/methods/email/3f7/verify" + let url = try resolver().url(forHref: href) + XCTAssertEqual(url.absoluteString, "https://login.microsoftonline.com/common/api/v0.1/auth/methods/email/3f7/verify") + } + + func test_url_forLeadingTenantSegmentHref_dropsTenantUsingApiMarker() throws { + let href = "/1eb974cd-0dc5-40a6-9f68-94b19f5535c5/api/v0.1/auth/methods/email/3f7/verify" + let url = try resolver().url(forHref: href) + XCTAssertEqual(url.absoluteString, "https://login.microsoftonline.com/common/api/v0.1/auth/methods/email/3f7/verify") + } + + func test_url_forHrefWithOauthMarker_dropsTenantUsingOauthMarker() throws { + let href = "/1eb974cd/oauth2/v2.0/token" + let url = try resolver().url(forHref: href) + XCTAssertEqual(url.absoluteString, "https://login.microsoftonline.com/common/oauth2/v2.0/token") + } + + func test_url_forHrefWithQuery_preservesHrefQuery() throws { + let href = "/tenant/api/v0.1/auth/methods/email/3f7/verify?dc=ESTS-PUB-SEASLR1" + let url = try resolver().url(forHref: href) + XCTAssertEqual( + url.absoluteString, + "https://login.microsoftonline.com/common/api/v0.1/auth/methods/email/3f7/verify?dc=ESTS-PUB-SEASLR1" + ) + } + + func test_url_forHrefWithExistingDc_whenDataCenterSet_doesNotDuplicateDc() throws { + let href = "/tenant/api/v0.1/auth/methods/email/3f7/verify?dc=ESTS-EXISTING" + let url = try resolver(dataCenter: "ESTS-NEW").url(forHref: href) + XCTAssertEqual( + url.absoluteString, + "https://login.microsoftonline.com/common/api/v0.1/auth/methods/email/3f7/verify?dc=ESTS-EXISTING" + ) + } + + func test_url_forRelativeHref_whenDataCenterSet_appendsDc() throws { + let href = "/tenant/api/v0.1/auth/methods/email/3f7/challenge" + let url = try resolver(dataCenter: "ESTS-NEW").url(forHref: href) + XCTAssertEqual( + url.absoluteString, + "https://login.microsoftonline.com/common/api/v0.1/auth/methods/email/3f7/challenge?dc=ESTS-NEW" + ) + } +} diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift new file mode 100644 index 0000000000..3339fd3f09 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift @@ -0,0 +1,201 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL + +final class MSALNativeAuthV2ParametersTests: XCTestCase { + + private var context: MSALNativeAuthRequestContext! + private let resolver = MSALNativeAuthV2HrefURLResolver( + authorityURL: URL(string: "https://login.microsoftonline.com/common")!, + dataCenter: nil + ) + + override func setUp() { + super.setUp() + context = MSALNativeAuthRequestContextMock() + } + + // MARK: - EntryParameters + + func test_entryParameters_body_url_andMetadata() throws { + let href = "/tenant/api/v0.1/auth/methods/signUp" + let sut = MSALNativeAuthV2EntryParameters( + context: context, + target: .href(href), + apiId: .telemetryApiIdV2SignUpStart, + operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, + username: "user@contoso.com", + continuationToken: "CT" + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignUpStart) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.signUpStart.rawValue) + XCTAssertEqual(sut.encoding, .json) + XCTAssertEqual(sut.httpMethod, "POST") + XCTAssertFalse(sut.expectsRawJSONResponse) + XCTAssertEqual(sut.body as? [String: String], ["username": "user@contoso.com", "continuationToken": "CT"]) + XCTAssertEqual(try sut.url(resolver: resolver), try resolver.url(forHref: href)) + } + + func test_entryParameters_whenTargetIsEndpoint_resolvesEndpointUrl() throws { + let sut = MSALNativeAuthV2EntryParameters( + context: context, + target: .endpoint(.authorizeChallenge), + apiId: .telemetryApiIdV2SignInWithCodeStart, + operationType: MSALNativeAuthV2OperationType.signInStart.rawValue, + username: "user@contoso.com", + continuationToken: "CT" + ) + + XCTAssertEqual(try sut.url(resolver: resolver), try resolver.url(for: .authorizeChallenge)) + } + + // MARK: - HrefParameters + + func test_hrefParameters_postWithOtp_body_url_andMetadata() throws { + let href = "/tenant/api/v0.1/auth/methods/email/3f7/verify" + let sut = MSALNativeAuthV2HrefParameters( + context: context, + href: href, + httpMethod: "POST", + apiId: .telemetryApiIdV2SignInSubmitCode, + operationType: MSALNativeAuthV2OperationType.verify.rawValue, + requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", otp: "1234") + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.verify.rawValue) + XCTAssertEqual(sut.encoding, .json) + XCTAssertEqual(sut.httpMethod, "POST") + XCTAssertFalse(sut.expectsRawJSONResponse) + XCTAssertEqual(sut.body as? [String: String], ["continuationToken": "CT", "otp": "1234"]) + XCTAssertEqual(try sut.url(resolver: resolver), try resolver.url(forHref: href)) + } + + func test_hrefParameters_putPassesHttpMethodThrough() throws { + let sut = MSALNativeAuthV2HrefParameters( + context: context, + href: "/tenant/api/v0.1/auth/methods/password/update", + httpMethod: "PUT", + apiId: .telemetryApiIdV2ResetPasswordSubmit, + operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, + requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", newPassword: "newPass") + ) + + XCTAssertEqual(sut.httpMethod, "PUT") + XCTAssertEqual(sut.body as? [String: String], ["continuationToken": "CT", "newPassword": "newPass"]) + } + + func test_hrefParameters_withAttributes_body() throws { + let sut = MSALNativeAuthV2HrefParameters( + context: context, + href: "/tenant/api/v0.1/auth/methods/attributes", + httpMethod: "POST", + apiId: .telemetryApiIdV2SignUpSubmitAttributes, + operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, + requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", attributes: ["city": "Redmond"]) + ) + + XCTAssertEqual(sut.body["continuationToken"] as? String, "CT") + XCTAssertEqual(sut.body["attributes"] as? [String: String], ["city": "Redmond"]) + } + + // MARK: - AuthorizeChallengeStartParameters + + func test_authorizeChallengeStartParameters_body_url_andMetadata() throws { + let sut = MSALNativeAuthV2AuthorizeChallengeStartParameters( + context: context, + clientId: "client-id", + apiId: .telemetryApiIdV2SignInWithCodeStart + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInWithCodeStart) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.authorizeChallengeStart.rawValue) + XCTAssertEqual(sut.encoding, .wwwFormUrlEncoded) + XCTAssertEqual(sut.httpMethod, "POST") + XCTAssertFalse(sut.expectsRawJSONResponse) + XCTAssertEqual(sut.body as? [String: String], ["client_id": "client-id"]) + XCTAssertEqual(try sut.url(resolver: resolver), try resolver.url(for: .authorizeChallenge)) + } + + // MARK: - AuthorizeChallengeContinueParameters + + func test_authorizeChallengeContinueParameters_body_url_andMetadata() throws { + let sut = MSALNativeAuthV2AuthorizeChallengeContinueParameters( + context: context, + continuationToken: "CT", + apiId: .telemetryApiIdV2SignInSubmitCode + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.authorizeChallengeContinue.rawValue) + XCTAssertEqual(sut.encoding, .wwwFormUrlEncoded) + XCTAssertEqual(sut.body as? [String: String], ["continuation_token": "CT"]) + XCTAssertEqual(try sut.url(resolver: resolver), try resolver.url(for: .authorizeChallenge)) + } + + // MARK: - TokenParameters + + func test_tokenParameters_withScopes_body_url_andMetadata() throws { + let sut = MSALNativeAuthV2TokenParameters( + context: context, + clientId: "client-id", + code: "auth-code", + scopes: ["scope1", "scope2"], + apiId: .telemetryApiIdV2SignInSubmitCode + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.token.rawValue) + XCTAssertEqual(sut.encoding, .wwwFormUrlEncoded) + XCTAssertTrue(sut.expectsRawJSONResponse) + XCTAssertEqual(sut.body as? [String: String], [ + "grant_type": "authorization_code", + "code": "auth-code", + "client_id": "client-id", + "client_info": "true", + "scope": "scope1 scope2" + ]) + XCTAssertEqual(try sut.url(resolver: resolver), try resolver.url(for: .token)) + } + + func test_tokenParameters_withoutScopes_omitsScope() throws { + let sut = MSALNativeAuthV2TokenParameters( + context: context, + clientId: "client-id", + code: "auth-code", + scopes: [], + apiId: .telemetryApiIdV2SignInSubmitCode + ) + + XCTAssertEqual(sut.body as? [String: String], [ + "grant_type": "authorization_code", + "code": "auth-code", + "client_id": "client-id", + "client_info": "true" + ]) + } +} diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift new file mode 100644 index 0000000000..bb8f8cac5b --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift @@ -0,0 +1,236 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +final class MSALNativeAuthV2RequestProviderTests: XCTestCase { + + private var sut: MSALNativeAuthV2RequestProvider! + private var resolver: MSALNativeAuthV2HrefURLResolver! + private var context: MSALNativeAuthRequestContext! + + private let href = "/tenant/api/v0.1/auth/methods/email/3f7/verify" + + override func setUp() { + super.setUp() + sut = MSALNativeAuthV2RequestProvider(config: MSALNativeAuthConfigStubs.configuration) + resolver = MSALNativeAuthV2HrefURLResolver(config: MSALNativeAuthConfigStubs.configuration) + context = MSALNativeAuthRequestContextMock() + } + + // MARK: - Helpers + + private func apiId(of request: MSIDHttpRequest) -> MSALNativeAuthTelemetryApiId? { + return (request.serverTelemetry as? MSALNativeAuthServerTelemetry)?.currentRequestTelemetry.apiId + } + + // MARK: - Entry requests + + func test_signUpStart_configuresHrefRequestAndThreadsApiId() throws { + let request = try sut.signUpStart( + username: "user@contoso.com", + continuationToken: "CT", + href: href, + apiId: .telemetryApiIdV2SignUpStart, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignUpStart) + XCTAssertTrue(request.responseSerializer is MSALNativeAuthV2HALResponseSerializer) + } + + func test_signInStart_threadsApiId() throws { + let request = try sut.signInStart( + username: "user@contoso.com", + continuationToken: "CT", + href: href, + apiId: .telemetryApiIdV2SignInWithCodeStart, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInWithCodeStart) + } + + func test_resetPasswordStart_threadsApiId() throws { + let request = try sut.resetPasswordStart( + username: "user@contoso.com", + continuationToken: "CT", + href: href, + apiId: .telemetryApiIdV2ResetPasswordStart, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordStart) + } + + // MARK: - HAL follow-up requests + + func test_submitPassword_threadsApiId() throws { + let request = try sut.submitPassword( + href: href, + password: "pass", + continuationToken: "CT", + apiId: .telemetryApiIdV2SignInSubmitPassword, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInSubmitPassword) + } + + func test_submitCode_threadsApiId() throws { + let request = try sut.submitCode( + href: href, + code: "1234", + continuationToken: "CT", + apiId: .telemetryApiIdV2SignInSubmitCode, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInSubmitCode) + } + + func test_submitAttributes_threadsApiId() throws { + let request = try sut.submitAttributes( + href: href, + attributes: ["city": "Redmond"], + continuationToken: "CT", + apiId: .telemetryApiIdV2SignUpSubmitAttributes, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignUpSubmitAttributes) + } + + func test_registerMethod_threadsApiId() throws { + let request = try sut.registerMethod( + href: href, + target: "email", + continuationToken: "CT", + apiId: .telemetryApiIdV2JITChallenge, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2JITChallenge) + } + + func test_challenge_threadsApiId() throws { + let request = try sut.challenge( + href: href, + continuationToken: "CT", + apiId: .telemetryApiIdV2ResetPasswordResendCode, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordResendCode) + } + + func test_verify_threadsApiId() throws { + let request = try sut.verify( + href: href, + otp: "1234", + continuationToken: "CT", + apiId: .telemetryApiIdV2MFASubmitChallenge, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2MFASubmitChallenge) + } + + func test_updatePassword_usesPutAndThreadsApiId() throws { + let request = try sut.updatePassword( + href: href, + newPassword: "newPass", + continuationToken: "CT", + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "PUT") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmit) + } + + func test_poll_threadsApiId() throws { + let request = try sut.poll( + href: href, + continuationToken: "CT", + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmit) + } + + // MARK: - Fixed-endpoint requests + + func test_authorizeChallengeStart_usesAuthorizeChallengeEndpointAndThreadsApiId() throws { + let request = try sut.authorizeChallengeStart( + apiId: .telemetryApiIdV2SignInWithPasswordStart, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .authorizeChallenge)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInWithPasswordStart) + } + + func test_authorizeChallengeContinue_usesAuthorizeChallengeEndpointAndThreadsApiId() throws { + let request = try sut.authorizeChallengeContinue( + continuationToken: "CT", + apiId: .telemetryApiIdV2SignInWithCodeStart, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .authorizeChallenge)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInWithCodeStart) + } + + func test_token_usesTokenEndpointAndKeepsRawJSONSerializer() throws { + let request = try sut.token( + code: "auth-code", + scopes: ["scope1"], + apiId: .telemetryApiIdV2SignInSubmitCode, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .token)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInSubmitCode) + XCTAssertFalse(request.responseSerializer is MSALNativeAuthV2HALResponseSerializer) + } +} From 71700ae13c34a57b23a404fe1a6fa8a24790cc1d Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 23 Jul 2026 14:54:32 +0100 Subject: [PATCH 66/85] Add V2 native-auth Tier 1 tests: FlowError, dispatcher, HAL serializer, error handler Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2 --- MSAL/MSAL.xcodeproj/project.pbxproj | 24 ++ ...ativeAuthFlowResponseDispatcherTests.swift | 224 ++++++++++++++++++ ...tiveAuthV2HALResponseSerializerTests.swift | 195 +++++++++++++++ ...ativeAuthV2ResponseErrorHandlerTests.swift | 113 +++++++++ .../v2/MSALNativeAuthFlowErrorTests.swift | 146 ++++++++++++ 5 files changed, 702 insertions(+) create mode 100644 MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift create mode 100644 MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift create mode 100644 MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift create mode 100644 MSAL/test/unit/native_auth/public/state_machine/v2/MSALNativeAuthFlowErrorTests.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 04cd6a501b..3825580824 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -418,6 +418,8 @@ 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; 38880DF423280C5900688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; 38880DF523280C5A00688C24 /* MSALPublicClientApplicationConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 23B1D35D22EA4797000954AF /* MSALPublicClientApplicationConfig.m */; }; + 3909B2CE15314B4B33F17289 /* MSALNativeAuthFlowErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C52731D0550F3D98B606302 /* MSALNativeAuthFlowErrorTests.swift */; }; + 3910135713EE25264B751FF5 /* MSALNativeAuthV2ResponseErrorHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A54EBC67992C2F90976346 /* MSALNativeAuthV2ResponseErrorHandlerTests.swift */; }; 3F2E65884A64B912E42B512D /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; 42E1FE910A9592561A2F44DC /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; @@ -448,6 +450,7 @@ 6525115A29CD84A000D3B876 /* MSALPublicClientApplicationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D673F07C1E4AAB0D0018BA91 /* MSALPublicClientApplicationTests.m */; }; 6534A6BDFED26846E71370A9 /* MSALNativeAuthAttributesRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */; }; 6577FFC829CC2E4B003235A6 /* MSALDeviceInfoProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B253153A23DD717900432133 /* MSALDeviceInfoProviderTests.m */; }; + 65F680460796E9A10FE8CD05 /* MSALNativeAuthFlowResponseDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */; }; 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 6A130FEA55D11486D2F4FA55 /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; @@ -515,6 +518,7 @@ 91AA247E2BDF6DC1005037EA /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9313B1799984552C778C5E5C /* MailTMHTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475F1413DA1D76D5EF31F4EC /* MailTMHTTPClient.swift */; }; 94E876CE1E492D6000FB96ED /* MSALAuthority.m in Sources */ = {isa = PBXBuildFile; fileRef = 94E876CB1E492D6000FB96ED /* MSALAuthority.m */; }; + 9531B6F096270D19F6E95596 /* MSALNativeAuthFlowErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C52731D0550F3D98B606302 /* MSALNativeAuthFlowErrorTests.swift */; }; 960751BB2183E82C00F2BF2F /* MSALAccountIdTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 960751BA2183E82C00F2BF2F /* MSALAccountIdTests.m */; }; 960751BC2183E82C00F2BF2F /* MSALAccountIdTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 960751BA2183E82C00F2BF2F /* MSALAccountIdTests.m */; }; 96090D9020E58DE600E42B37 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96902DEC20E1574F00200E6F /* WebKit.framework */; }; @@ -675,6 +679,7 @@ B227037122A4BA3600030ADC /* MSALLegacySharedAccountsProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = B29A56BD228266E20023F5E6 /* MSALLegacySharedAccountsProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; B227037322A4BA3E00030ADC /* MSALLegacySharedAccountsProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B29A56BE228266E20023F5E6 /* MSALLegacySharedAccountsProvider.m */; }; B227557C23752545000B7EF3 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2EE86E223751CAE00D0BC96 /* AuthenticationServices.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + B241DD9BDD1D50BFBAC9BEFF /* MSALNativeAuthV2ResponseErrorHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A54EBC67992C2F90976346 /* MSALNativeAuthV2ResponseErrorHandlerTests.swift */; }; B2472CA3226FDC46008F22AB /* MSALB2CAuthority_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2472CA2226FDC46008F22AB /* MSALB2CAuthority_Internal.h */; }; B2472CA4226FDC46008F22AB /* MSALB2CAuthority_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2472CA2226FDC46008F22AB /* MSALB2CAuthority_Internal.h */; }; B2472CA5226FDC46008F22AB /* MSALB2CAuthority_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2472CA2226FDC46008F22AB /* MSALB2CAuthority_Internal.h */; }; @@ -1044,7 +1049,9 @@ B5A1E2121EE36D3BC037113D /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; BCC3280FFD148F8A55084523 /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; BE5CFDC45CA2EB0EC61A9A85 /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; + C277EAF06922901997D9D450 /* MSALNativeAuthV2HALResponseSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065DECC57E9CF4618C1D5494 /* MSALNativeAuthV2HALResponseSerializerTests.swift */; }; C34EB4B71143A1078F0B72E2 /* MSALNativeAuthV2ParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */; }; + C3B881EFA8EC0E8B506F576D /* MSALNativeAuthV2HALResponseSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 065DECC57E9CF4618C1D5494 /* MSALNativeAuthV2HALResponseSerializerTests.swift */; }; C4675E1CCC8208251CE74818 /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; @@ -1778,6 +1785,7 @@ F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; + F819D42E8772D0CDAB08945A /* MSALNativeAuthFlowResponseDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; FADE0000000000000000AA03 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; @@ -2138,9 +2146,11 @@ 04D32CAC1FD61585000B123E /* MSALErrorConverter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALErrorConverter.h; sourceTree = ""; }; 04D32CAD1FD615B3000B123E /* MSALErrorConverter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALErrorConverter.m; sourceTree = ""; }; 04D32CCF1FD8AFF3000B123E /* MSALErrorConverterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALErrorConverterTests.m; sourceTree = ""; }; + 065DECC57E9CF4618C1D5494 /* MSALNativeAuthV2HALResponseSerializerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MSALNativeAuthV2HALResponseSerializerTests.swift; path = ../responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift; sourceTree = ""; }; 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2LinkRelation.swift; sourceTree = ""; }; 0D96DB2E27850E1300DEAF87 /* MSALWipeCacheForAllAccountsConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALWipeCacheForAllAccountsConfig.h; sourceTree = ""; }; 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWipeCacheForAllAccountsConfig.m; sourceTree = ""; }; + 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowResponseDispatcherTests.swift; sourceTree = ""; }; 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorityURLFormat.swift; sourceTree = ""; }; 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolverTests.swift; sourceTree = ""; }; 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBody.swift; sourceTree = ""; }; @@ -2229,6 +2239,7 @@ 23F32F051FF4787600B2905E /* MSIDTestURLResponse+MSAL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSIDTestURLResponse+MSAL.h"; sourceTree = ""; }; 23F32F061FF4787600B2905E /* MSIDTestURLResponse+MSAL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSIDTestURLResponse+MSAL.m"; sourceTree = ""; }; 23FB5C1C22542B99002BF1EB /* MSALJsonDeserializable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALJsonDeserializable.h; sourceTree = ""; }; + 26A54EBC67992C2F90976346 /* MSALNativeAuthV2ResponseErrorHandlerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MSALNativeAuthV2ResponseErrorHandlerTests.swift; path = ../responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift; sourceTree = ""; }; 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2EntryParameters.swift; sourceTree = ""; }; 280095EA2C32CAFC00F1653E /* ClientIdType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIdType.swift; sourceTree = ""; }; 2809E8342C3C37B7009F14D7 /* MSALNativeAuthEndToEndPasswordTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthEndToEndPasswordTestCase.swift; sourceTree = ""; }; @@ -2338,6 +2349,7 @@ 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 58B81F6524AC59A000E8799E /* MSALTestCacheTokenResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTestCacheTokenResponse.h; sourceTree = ""; }; 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTestCacheTokenResponse.m; sourceTree = ""; }; + 5C52731D0550F3D98B606302 /* MSALNativeAuthFlowErrorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MSALNativeAuthFlowErrorTests.swift; path = ../../public/state_machine/v2/MSALNativeAuthFlowErrorTests.swift; sourceTree = ""; }; 6077D49F22498BFF001798A2 /* MSALTenantProfile.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTenantProfile.h; sourceTree = ""; }; 6077D4A822498D87001798A2 /* MSALTenantProfile.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTenantProfile.m; sourceTree = ""; }; 609AF9322256BD0C00E2978D /* MSALAccountsProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAccountsProviderTests.m; sourceTree = ""; }; @@ -3679,6 +3691,9 @@ 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */, C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */, 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */, + 5C52731D0550F3D98B606302 /* MSALNativeAuthFlowErrorTests.swift */, + 065DECC57E9CF4618C1D5494 /* MSALNativeAuthV2HALResponseSerializerTests.swift */, + 26A54EBC67992C2F90976346 /* MSALNativeAuthV2ResponseErrorHandlerTests.swift */, ); name = v2; path = v2; @@ -5590,6 +5605,7 @@ isa = PBXGroup; children = ( 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */, + 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */, ); name = v2; path = v2; @@ -8055,6 +8071,10 @@ 192F74D7E3825C5CDCF50CEB /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */, 547D9B6A1EA16110560F531F /* MSALNativeAuthV2ParametersTests.swift in Sources */, B4CDF4FB20138CF27310258B /* MSALNativeAuthV2RequestProviderTests.swift in Sources */, + F819D42E8772D0CDAB08945A /* MSALNativeAuthFlowResponseDispatcherTests.swift in Sources */, + 3909B2CE15314B4B33F17289 /* MSALNativeAuthFlowErrorTests.swift in Sources */, + C3B881EFA8EC0E8B506F576D /* MSALNativeAuthV2HALResponseSerializerTests.swift in Sources */, + 3910135713EE25264B751FF5 /* MSALNativeAuthV2ResponseErrorHandlerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8241,6 +8261,10 @@ 827CE360F94F0A5BCA875193 /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */, C34EB4B71143A1078F0B72E2 /* MSALNativeAuthV2ParametersTests.swift in Sources */, D3C4A02BF6F6E02B8D58ACE8 /* MSALNativeAuthV2RequestProviderTests.swift in Sources */, + 65F680460796E9A10FE8CD05 /* MSALNativeAuthFlowResponseDispatcherTests.swift in Sources */, + 9531B6F096270D19F6E95596 /* MSALNativeAuthFlowErrorTests.swift in Sources */, + C277EAF06922901997D9D450 /* MSALNativeAuthV2HALResponseSerializerTests.swift in Sources */, + B241DD9BDD1D50BFBAC9BEFF /* MSALNativeAuthV2ResponseErrorHandlerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift new file mode 100644 index 0000000000..3f515ad55f --- /dev/null +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift @@ -0,0 +1,224 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +final class MSALNativeAuthFlowResponseDispatcherTests: XCTestCase { + + private let sut = MSALNativeAuthFlowResponseDispatcher() + + // MARK: - completed + + func test_dispatch_completed_callsOnFlowCompletedAndTelemetry() async { + let delegate = BaseDelegateSpy() + var telemetryResult: Result? + let response = MSALNativeAuthFlowControllerResponse( + .completed(MSALNativeAuthUserAccountResultStub.result), + correlationId: UUID(), + scenario: .signIn, + telemetryUpdate: { telemetryResult = $0 } + ) + + await sut.dispatch(response, delegate: delegate) + + XCTAssertEqual(delegate.completedScenario, .signIn) + XCTAssertNil(delegate.error) + assertTelemetrySuccess(telemetryResult) + } + + // MARK: - error + + func test_dispatch_error_callsOnFlowErrorAndDoesNotFireTelemetry() async { + let delegate = BaseDelegateSpy() + var telemetryFired = false + let error = MSALNativeAuthFlowError(type: .invalidCode) + let response = MSALNativeAuthFlowControllerResponse( + .error(error: error, newState: nil), + correlationId: UUID(), + scenario: .passwordReset, + telemetryUpdate: { _ in telemetryFired = true } + ) + + await sut.dispatch(response, delegate: delegate) + + XCTAssertTrue(delegate.error === error) + XCTAssertEqual(delegate.errorScenario, .passwordReset) + XCTAssertFalse(telemetryFired) + } + + // MARK: - browserRequired + + func test_dispatch_browserRequired_callsOnFlowErrorWithBrowserRequiredAndTelemetry() async { + let delegate = BaseDelegateSpy() + var telemetryResult: Result? + let response = MSALNativeAuthFlowControllerResponse( + .browserRequired(url: URL(string: "https://contoso.com/fallback")!, newState: makeInternalState()), + correlationId: UUID(), + scenario: .signUp, + telemetryUpdate: { telemetryResult = $0 } + ) + + await sut.dispatch(response, delegate: delegate) + + XCTAssertEqual(delegate.errorScenario, .signUp) + XCTAssertTrue(delegate.error?.isBrowserRequired ?? false) + assertTelemetrySuccess(telemetryResult) + } + + // MARK: - actionRequired: delegate conforms + + func test_dispatch_actionRequired_conformingDelegate_callsTypedCallbackAndTelemetry() async { + let delegate = CodeRequiredDelegateSpy() + let internalState = makeInternalState(scenario: .signUp) + let state = MSALNativeAuthCodeRequiredState( + internalState: internalState, + sentTo: "u***@contoso.com", + channel: MSALNativeAuthChannelType(value: "email"), + codeLength: 8 + ) + var telemetryResult: Result? + let response = MSALNativeAuthFlowControllerResponse( + .actionRequired(state: state), + correlationId: UUID(), + scenario: .unknown, + telemetryUpdate: { telemetryResult = $0 } + ) + + await sut.dispatch(response, delegate: delegate) + + XCTAssertTrue(delegate.codeRequiredState === state) + // The scenario is taken from the state's continuation, not from response.scenario. + XCTAssertEqual(delegate.codeRequiredScenario, .signUp) + XCTAssertNil(delegate.error) + assertTelemetrySuccess(telemetryResult) + } + + // MARK: - actionRequired: delegate does not conform + + func test_dispatch_actionRequired_nonConformingDelegate_callsNotImplementedAndSkipsTelemetry() async { + let delegate = BaseDelegateSpy() + let internalState = makeInternalState(scenario: .signIn) + let state = MSALNativeAuthCodeRequiredState( + internalState: internalState, + sentTo: "u***@contoso.com", + channel: MSALNativeAuthChannelType(value: "email"), + codeLength: 8 + ) + var telemetryFired = false + let response = MSALNativeAuthFlowControllerResponse( + .actionRequired(state: state), + correlationId: UUID(), + scenario: .unknown, + telemetryUpdate: { _ in telemetryFired = true } + ) + + await sut.dispatch(response, delegate: delegate) + + XCTAssertEqual(delegate.errorScenario, .signIn) + XCTAssertTrue(delegate.error?.isNotImplemented ?? false) + XCTAssertFalse(telemetryFired) + } + + // MARK: - actionRequired: routes each concrete state to its own callback + + func test_dispatch_actionRequired_passwordRequiredState_routesToPasswordCallback() async { + let delegate = PasswordRequiredDelegateSpy() + let state = MSALNativeAuthPasswordRequiredState(internalState: makeInternalState(scenario: .signIn)) + let response = MSALNativeAuthFlowControllerResponse( + .actionRequired(state: state), + correlationId: UUID(), + scenario: .unknown + ) + + await sut.dispatch(response, delegate: delegate) + + XCTAssertTrue(delegate.passwordRequiredState === state) + XCTAssertEqual(delegate.passwordRequiredScenario, .signIn) + XCTAssertNil(delegate.error) + } + + // MARK: - Helpers + + private func makeInternalState(scenario: MSALNativeAuthFlowScenario = .signIn) -> MSALNativeAuthFlowInternalState { + let continuation = MSALNativeAuthFlowContinuationState( + flowScenario: scenario, + continuationToken: "ct", + links: [:], + username: nil + ) + return MSALNativeAuthFlowInternalState(continuation: continuation, controller: MSALNativeAuthFlowControllerMock()) + } + + private func assertTelemetrySuccess( + _ result: Result?, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard case .success = result else { + return XCTFail("Expected telemetry success", file: file, line: line) + } + } +} + +// MARK: - Delegate spies + +private class BaseDelegateSpy: NSObject, MSALNativeAuthFlowDelegate { + + var completedScenario: MSALNativeAuthFlowScenario? + var error: MSALNativeAuthFlowError? + var errorScenario: MSALNativeAuthFlowScenario? + + func onFlowCompleted(result: MSALNativeAuthUserAccountResult, scenario: MSALNativeAuthFlowScenario) { + completedScenario = scenario + } + + func onFlowError(error: MSALNativeAuthFlowError, scenario: MSALNativeAuthFlowScenario) { + self.error = error + errorScenario = scenario + } +} + +private final class CodeRequiredDelegateSpy: BaseDelegateSpy, MSALNativeAuthCodeRequiredDelegate { + + var codeRequiredState: MSALNativeAuthCodeRequiredState? + var codeRequiredScenario: MSALNativeAuthFlowScenario? + + func onCodeRequired(state: MSALNativeAuthCodeRequiredState, scenario: MSALNativeAuthFlowScenario) { + codeRequiredState = state + codeRequiredScenario = scenario + } +} + +private final class PasswordRequiredDelegateSpy: BaseDelegateSpy, MSALNativeAuthPasswordRequiredDelegate { + + var passwordRequiredState: MSALNativeAuthPasswordRequiredState? + var passwordRequiredScenario: MSALNativeAuthFlowScenario? + + func onPasswordRequired(state: MSALNativeAuthPasswordRequiredState, scenario: MSALNativeAuthFlowScenario) { + passwordRequiredState = state + passwordRequiredScenario = scenario + } +} diff --git a/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift new file mode 100644 index 0000000000..5983fb4460 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift @@ -0,0 +1,195 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +final class MSALNativeAuthV2HALResponseSerializerTests: XCTestCase { + + private let sut = MSALNativeAuthV2HALResponseSerializer() + + // MARK: - Top-level scalar fields + + func test_responseObject_parsesTopLevelScalarFields() throws { + let json: [String: Any] = [ + "state": "interactionRequired", + "action": "challenge", + "continuation_token": "ct-123", + "codeLength": 8, + "hint": "u***@contoso.com", + "id": "method-1", + "type": "email", + "code": "auth-code", + "challengeContext": ["authenticationFactor": "oob"] + ] + + let response = try parse(json, statusCode: 200) + + XCTAssertEqual(response.statusCode, 200) + XCTAssertEqual(response.state, "interactionRequired") + XCTAssertEqual(response.action, "challenge") + XCTAssertEqual(response.continuationToken, "ct-123") + XCTAssertEqual(response.codeLength, 8) + XCTAssertEqual(response.hint, "u***@contoso.com") + XCTAssertEqual(response.methodId, "method-1") + XCTAssertEqual(response.methodType, "email") + XCTAssertEqual(response.code, "auth-code") + XCTAssertEqual(response.authenticationFactor, "oob") + } + + func test_responseObject_prefersCamelCaseContinuationToken() throws { + let response = try parse(["continuationToken": "camel", "continuation_token": "snake"], statusCode: 200) + XCTAssertEqual(response.continuationToken, "camel") + } + + // MARK: - Links + + func test_responseObject_parsesTopLevelLinks() throws { + let json: [String: Any] = [ + "_links": [ + "verify": ["href": "https://contoso.com/verify", "name": "verify"], + "resend": ["href": "https://contoso.com/challenge"] + ] + ] + + let response = try parse(json, statusCode: 200) + + XCTAssertEqual(response.href(forRelation: "verify"), "https://contoso.com/verify") + XCTAssertEqual(response.href(forRelation: "resend"), "https://contoso.com/challenge") + } + + func test_responseObject_parsesAuthorizeChallengeFlowLinksFromTopLevelJSON() throws { + let response = try parse(["sign_in": "https://contoso.com/signin"], statusCode: 401) + XCTAssertEqual(response.href(forRelation: "sign_in"), "https://contoso.com/signin") + } + + // MARK: - Embedded methods + + func test_responseObject_parsesEmbeddedMethods() throws { + let json: [String: Any] = [ + "_embedded": [ + "methods": [ + [ + "id": "1", + "type": "email", + "hint": "u***@contoso.com", + "_links": ["challenge": ["href": "https://contoso.com/challenge"]] + ] + ] + ] + ] + + let response = try parse(json, statusCode: 200) + + XCTAssertEqual(response.methods.count, 1) + let method = try XCTUnwrap(response.methods.first) + XCTAssertEqual(method.id, "1") + XCTAssertEqual(method.type, "email") + XCTAssertEqual(method.hint, "u***@contoso.com") + XCTAssertEqual(method.link(for: .challenge), "https://contoso.com/challenge") + } + + // MARK: - Attributes + + func test_responseObject_parsesAttributes() throws { + let json: [String: Any] = [ + "attributes": [ + ["attributeId": "email", "type": "string", "required": true, "validationRegex": ".+@.+"], + ["id": "displayName", "type": "string"] + ] + ] + + let response = try parse(json, statusCode: 200) + + XCTAssertEqual(response.attributes.count, 2) + XCTAssertEqual(response.attributes[0], .init(id: "email", type: "string", required: true, regex: ".+@.+")) + XCTAssertEqual(response.attributes[1], .init(id: "displayName", type: "string", required: false, regex: nil)) + } + + // MARK: - Server error + + func test_responseObject_parsesServerError() throws { + let json: [String: Any] = [ + "error": [ + "code": "invalid_grant", + "message": "bad code", + "innerError": ["code": "invalid_oob_value"] + ] + ] + + let response = try parse(json, statusCode: 400) + + let error = try XCTUnwrap(response.error) + XCTAssertEqual(error.code, "invalid_grant") + XCTAssertEqual(error.message, "bad code") + XCTAssertEqual(error.innerErrorCode, "invalid_oob_value") + } + + func test_responseObject_serverErrorCorrelationIdParsedFromBody() throws { + let correlationId = UUID() + let json: [String: Any] = ["error": ["code": "x", "correlation_id": correlationId.uuidString]] + + let response = try parse(json, statusCode: 400) + + XCTAssertEqual(response.error?.correlationId, correlationId) + } + + // MARK: - Empty / malformed bodies + + func test_responseObject_emptyData_returnsEmptyResponseWithStatusCode() throws { + let httpResponse = HTTPURLResponse(url: url, statusCode: 204, httpVersion: nil, headerFields: nil) + let result = try sut.responseObject(for: httpResponse, data: Data(), context: nil) + let response = try XCTUnwrap(result as? MSALNativeAuthHALResponse) + + XCTAssertEqual(response.statusCode, 204) + XCTAssertNil(response.state) + XCTAssertNil(response.action) + XCTAssertNil(response.error) + XCTAssertTrue(response.links.isEmpty) + XCTAssertTrue(response.methods.isEmpty) + } + + func test_responseObject_nilHTTPResponse_defaultsStatusCodeToZero() throws { + let result = try sut.responseObject(for: nil, data: Data(), context: nil) + let response = try XCTUnwrap(result as? MSALNativeAuthHALResponse) + XCTAssertEqual(response.statusCode, 0) + } + + func test_responseObject_nonJSONBody_throws() { + let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + XCTAssertThrowsError(try sut.responseObject(for: httpResponse, data: Data("not json".utf8), context: nil)) + } + + // MARK: - Helpers + + private let url = URL(string: "https://contoso.com/api/v0.1/auth")! + + private func parse(_ json: [String: Any], statusCode: Int) throws -> MSALNativeAuthHALResponse { + let data = try JSONSerialization.data(withJSONObject: json) + let httpResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil) + let result = try sut.responseObject(for: httpResponse, data: data, context: nil) + return try XCTUnwrap(result as? MSALNativeAuthHALResponse) + } +} diff --git a/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift new file mode 100644 index 0000000000..9e38b80971 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift @@ -0,0 +1,113 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +final class MSALNativeAuthV2ResponseErrorHandlerTests: XCTestCase { + + private let sut = MSALNativeAuthV2ResponseErrorHandler() + private let url = URL(string: "https://contoso.com/api/v0.1/auth")! + + func test_handleError_parsesBodyAndReturnsHALResponse() throws { + let json: [String: Any] = ["error": ["code": "invalid_grant", "message": "bad code"]] + let data = try JSONSerialization.data(withJSONObject: json) + let httpResponse = HTTPURLResponse(url: url, statusCode: 400, httpVersion: nil, headerFields: nil) + + let expectation = expectation(description: "completion called") + var receivedResponse: MSALNativeAuthHALResponse? + var receivedError: Error? + + sut.handleError( + nil, + httpResponse: httpResponse, + data: data, + httpRequest: nil, + responseSerializer: nil, + externalSSOContext: nil, + context: nil + ) { responseObject, error in + receivedResponse = responseObject as? MSALNativeAuthHALResponse + receivedError = error + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertNil(receivedError) + XCTAssertEqual(receivedResponse?.statusCode, 400) + XCTAssertEqual(receivedResponse?.error?.code, "invalid_grant") + } + + func test_handleError_usesProvidedSerializer() throws { + let json: [String: Any] = ["state": "continue"] + let data = try JSONSerialization.data(withJSONObject: json) + let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + + let expectation = expectation(description: "completion called") + var receivedResponse: MSALNativeAuthHALResponse? + + sut.handleError( + nil, + httpResponse: httpResponse, + data: data, + httpRequest: nil, + responseSerializer: MSALNativeAuthV2HALResponseSerializer(), + externalSSOContext: nil, + context: nil + ) { responseObject, _ in + receivedResponse = responseObject as? MSALNativeAuthHALResponse + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(receivedResponse?.state, "continue") + } + + func test_handleError_nonJSONBody_returnsError() { + let httpResponse = HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil) + + let expectation = expectation(description: "completion called") + var receivedResponse: Any? + var receivedError: Error? + + sut.handleError( + nil, + httpResponse: httpResponse, + data: Data("not json".utf8), + httpRequest: nil, + responseSerializer: nil, + externalSSOContext: nil, + context: nil + ) { responseObject, error in + receivedResponse = responseObject + receivedError = error + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertNil(receivedResponse) + XCTAssertNotNil(receivedError) + } +} diff --git a/MSAL/test/unit/native_auth/public/state_machine/v2/MSALNativeAuthFlowErrorTests.swift b/MSAL/test/unit/native_auth/public/state_machine/v2/MSALNativeAuthFlowErrorTests.swift new file mode 100644 index 0000000000..7bc9b687db --- /dev/null +++ b/MSAL/test/unit/native_auth/public/state_machine/v2/MSALNativeAuthFlowErrorTests.swift @@ -0,0 +1,146 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +@testable import MSAL +@_implementationOnly import MSAL_Private + +final class MSALNativeAuthFlowErrorTests: XCTestCase { + + // MARK: - Classification booleans + + func test_isNotImplemented_onlyTrueForNotImplementedType() { + assertClassification(matchingType: .notImplemented) { $0.isNotImplemented } + } + + func test_isUserNotFound_onlyTrueForUserNotFoundType() { + assertClassification(matchingType: .userNotFound) { $0.isUserNotFound } + } + + func test_isInvalidCode_onlyTrueForInvalidCodeType() { + assertClassification(matchingType: .invalidCode) { $0.isInvalidCode } + } + + func test_isInvalidPassword_onlyTrueForInvalidPasswordType() { + assertClassification(matchingType: .invalidPassword) { $0.isInvalidPassword } + } + + func test_isInvalidCredentials_onlyTrueForInvalidCredentialsType() { + assertClassification(matchingType: .invalidCredentials) { $0.isInvalidCredentials } + } + + func test_isInvalidUsername_onlyTrueForInvalidUsernameType() { + assertClassification(matchingType: .invalidUsername) { $0.isInvalidUsername } + } + + func test_isUserDoesNotHavePassword_onlyTrueForUserDoesNotHavePasswordType() { + assertClassification(matchingType: .userDoesNotHavePassword) { $0.isUserDoesNotHavePassword } + } + + func test_isUserAlreadyExists_onlyTrueForUserAlreadyExistsType() { + assertClassification(matchingType: .userAlreadyExists) { $0.isUserAlreadyExists } + } + + func test_isInvalidChallenge_onlyTrueForInvalidChallengeType() { + assertClassification(matchingType: .invalidChallenge) { $0.isInvalidChallenge } + } + + func test_isAuthMethodBlocked_onlyTrueForAuthMethodBlockedType() { + assertClassification(matchingType: .authMethodBlocked) { $0.isAuthMethodBlocked } + } + + func test_isVerificationContactBlocked_onlyTrueForVerificationContactBlockedType() { + assertClassification(matchingType: .verificationContactBlocked) { $0.isVerificationContactBlocked } + } + + func test_isInvalidInput_onlyTrueForInvalidInputType() { + assertClassification(matchingType: .invalidInput) { $0.isInvalidInput } + } + + // MARK: - Browser / general flags forwarded to the base error + + func test_browserRequiredType_setsIsBrowserRequired() { + let error = MSALNativeAuthFlowError(type: .browserRequired) + XCTAssertTrue(error.isBrowserRequired) + XCTAssertFalse(error.isGeneralError) + } + + func test_generalErrorType_setsIsGeneralError() { + let error = MSALNativeAuthFlowError(type: .generalError) + XCTAssertTrue(error.isGeneralError) + XCTAssertFalse(error.isBrowserRequired) + } + + // MARK: - errorDescription + + func test_errorDescription_usesProvidedDescriptionWhenPresent() { + let error = MSALNativeAuthFlowError(type: .invalidCode, errorDescription: "custom message") + XCTAssertEqual(error.errorDescription, "custom message") + } + + func test_errorDescription_fallsBackToTypeMessageWhenNoDescription() { + XCTAssertEqual(MSALNativeAuthFlowError(type: .notImplemented).errorDescription, MSALNativeAuthErrorMessage.delegateNotImplementedV2) + XCTAssertEqual(MSALNativeAuthFlowError(type: .userNotFound).errorDescription, MSALNativeAuthErrorMessage.userNotFound) + XCTAssertEqual(MSALNativeAuthFlowError(type: .invalidCode).errorDescription, MSALNativeAuthErrorMessage.invalidCode) + XCTAssertEqual(MSALNativeAuthFlowError(type: .invalidPassword).errorDescription, MSALNativeAuthErrorMessage.invalidPassword) + XCTAssertEqual(MSALNativeAuthFlowError(type: .invalidCredentials).errorDescription, MSALNativeAuthErrorMessage.invalidCredentials) + XCTAssertEqual(MSALNativeAuthFlowError(type: .invalidUsername).errorDescription, MSALNativeAuthErrorMessage.invalidUsername) + XCTAssertEqual(MSALNativeAuthFlowError(type: .userDoesNotHavePassword).errorDescription, MSALNativeAuthErrorMessage.userDoesNotHavePassword) + XCTAssertEqual(MSALNativeAuthFlowError(type: .userAlreadyExists).errorDescription, MSALNativeAuthErrorMessage.userAlreadyExists) + XCTAssertEqual(MSALNativeAuthFlowError(type: .invalidChallenge).errorDescription, MSALNativeAuthErrorMessage.invalidChallenge) + XCTAssertEqual(MSALNativeAuthFlowError(type: .authMethodBlocked).errorDescription, MSALNativeAuthErrorMessage.authMethodBlocked) + XCTAssertEqual(MSALNativeAuthFlowError(type: .verificationContactBlocked).errorDescription, MSALNativeAuthErrorMessage.verificationContactBlocked) + XCTAssertEqual(MSALNativeAuthFlowError(type: .invalidInput).errorDescription, MSALNativeAuthErrorMessage.invalidInput) + XCTAssertEqual(MSALNativeAuthFlowError(type: .browserRequired).errorDescription, MSALNativeAuthErrorMessage.browserRequired) + XCTAssertEqual(MSALNativeAuthFlowError(type: .generalError).errorDescription, MSALNativeAuthErrorMessage.generalError) + } + + // MARK: - Initializers + + func test_designatedInit_preservesCorrelationIdAndErrorCodes() { + let correlationId = UUID() + let error = MSALNativeAuthFlowError(type: .invalidCode, errorCodes: [50034], correlationId: correlationId) + XCTAssertEqual(error.correlationId, correlationId) + XCTAssertEqual(error.errorCodes, [50034]) + } + + func test_convenienceInit_generatesCorrelationId() { + let error = MSALNativeAuthFlowError(type: .invalidCode) + XCTAssertNotNil(error.correlationId) + } + + // MARK: - Helpers + + private func assertClassification( + matchingType: MSALNativeAuthFlowError.ErrorType, + _ predicate: (MSALNativeAuthFlowError) -> Bool, + file: StaticString = #filePath, + line: UInt = #line + ) { + for type in MSALNativeAuthFlowError.ErrorType.allCases { + let error = MSALNativeAuthFlowError(type: type) + XCTAssertEqual(predicate(error), type == matchingType, "type \(type)", file: file, line: line) + } + } +} From 1711f5b8daf1ea5cbeb7d6656a88496ad930be75 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 23 Jul 2026 16:16:19 +0100 Subject: [PATCH 67/85] Remove non sspr --- .../MSALNativeAuthFlowContinuationState.swift | 19 +- .../v2/MSALNativeAuthFlowController.swift | 624 +----------------- ...MSALNativeAuthFlowResponseDispatcher.swift | 28 - .../MSALNativeAuthV2ResponseValidator.swift | 67 +- .../MSALNativeAuthV2ValidatedResponses.swift | 26 - .../v2/MSALNativeAuthV2RequestBody.swift | 16 - .../v2/MSALNativeAuthV2RequestBodyKey.swift | 3 - .../v2/MSALNativeAuthV2RequestProvider.swift | 144 ---- .../MSALNativeAuthFlowControllerTests.swift | 254 ------- ...ativeAuthFlowResponseDispatcherTests.swift | 29 - .../MSALNativeAuthV2RequestProviderMock.swift | 84 --- .../v2/MSALNativeAuthV2ParametersTests.swift | 46 +- ...MSALNativeAuthV2RequestProviderTests.swift | 99 +-- ...ALNativeAuthV2ResponseValidatorTests.swift | 19 - 14 files changed, 61 insertions(+), 1397 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift index badc5609d0..e6b674b369 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -42,20 +42,9 @@ struct MSALNativeAuthFlowContinuationState { let username: String? let sentToHint: String? let codeLength: Int? - let authMethods: [MSALAuthMethod] /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final /// `/token` exchange. Threaded through every step. let scopes: [String] - /// Values supplied by the app at sign-up start (keyed by attribute id, e.g. "email"/"password") - /// that the SDK submits automatically when the server issues a `collectAttributes` request for - /// them. Deliberately kept internal so the app never sees them again; must never be logged or - /// exposed on the public surface. - let signUpAutofillValues: [String: Any]? - /// Attribute ids already auto-submitted from ``signUpAutofillValues`` during this sign-up flow. - /// Used to detect when the server re-requests an attribute we already sent (e.g. after a - /// validation failure) so the SDK surfaces an error to the app instead of resending in a loop. - /// Carries no attribute values. - let signUpAutofillSubmittedIds: Set init( flowScenario: MSALNativeAuthFlowScenario, @@ -64,10 +53,7 @@ struct MSALNativeAuthFlowContinuationState { username: String?, sentToHint: String? = nil, codeLength: Int? = nil, - authMethods: [MSALAuthMethod] = [], - scopes: [String] = [], - signUpAutofillValues: [String: Any]? = nil, - signUpAutofillSubmittedIds: Set = [] + scopes: [String] = [] ) { self.flowScenario = flowScenario self.continuationToken = continuationToken @@ -75,10 +61,7 @@ struct MSALNativeAuthFlowContinuationState { self.username = username self.sentToHint = sentToHint self.codeLength = codeLength - self.authMethods = authMethods self.scopes = scopes - self.signUpAutofillValues = signUpAutofillValues - self.signUpAutofillSubmittedIds = signUpAutofillSubmittedIds } func link(_ relation: MSALNativeAuthV2LinkRelation) -> URL? { diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 9c38daaf3c..d700cb11fb 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -66,159 +66,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MARK: - Entry points func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse { - let flowScenario: MSALNativeAuthFlowScenario = .signUp - let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpStart, context: context) - let scopes = joinScopes(parameters.scopes) - - // Authorization challenge (expects 401 + continuation token + sign_up link). - let authorizationChallenge = await performAuthorizeChallengeStart( - flowScenario: flowScenario, - apiId: .telemetryApiIdV2SignUpStart, - context: context - ) - guard case .continuationToken(let continuationToken, let signUpLink) = authorizationChallenge else { - return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) - } - - let startResult = await performInteraction(context: context) { - try self.requestProvider.signUpStart( - username: parameters.username, - continuationToken: continuationToken, - href: signUpLink, - apiId: .telemetryApiIdV2SignUpStart, - context: context - ) - } - - // The APIs request attributes at specific parts of the SingUp process - // so they must be carried privately for the whole flow - - // TODO: Remove Email, it should not be kept, but there's a bug on API side - var autofillValues: [String: Any] = ["email": parameters.username] - if let attributes = parameters.attributes { - autofillValues.merge(attributes) { _, new in new } - } - - // TODO: Confirm password needs to be sent as attribute - if let password = parameters.password { - autofillValues["password"] = password - } - - return await mapInteraction( - startResult, - flowScenario: flowScenario, - username: parameters.username, - scopes: scopes, - apiId: .telemetryApiIdV2SignUpStart, - event: event, - context: context, - signUpAutofillValues: autofillValues - ) + return notImplementedResponse(scenario: .signUp) } - // swiftlint:disable:next function_body_length func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse { - let flowScenario: MSALNativeAuthFlowScenario = .signIn - let context = MSALNativeAuthRequestContext(correlationId: parameters.correlationId) - let apiId: MSALNativeAuthTelemetryApiId = parameters.password != nil - ? .telemetryApiIdV2SignInWithPasswordStart - : .telemetryApiIdV2SignInWithCodeStart - let event = makeAndStartTelemetryEvent(id: apiId, context: context) - let scopes = joinScopes(parameters.scopes) - - // Authorization challenge (expects 401 + continuation token + sign_in link). - let authorizationChallenge = await performAuthorizeChallengeStart(flowScenario: flowScenario, apiId: apiId, context: context) - guard case .continuationToken(let continuationToken, let signInLink) = authorizationChallenge else { - return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) - } - - let startResult = await performInteraction(context: context) { - try self.requestProvider.signInStart( - username: parameters.username, - continuationToken: continuationToken, - href: signInLink, - apiId: apiId, - context: context - ) - } - - let challengeContinuationToken: String - let challengeHref: String - let challengeHint: String? - - switch startResult { - case .signInMethods(let token, let methods): - // Pick a method: password when a password was supplied, otherwise the first OTP method. - let passwordMethod = methods.first { ($0.type ?? "") == "password" } - let otpMethod = methods.first { ($0.type ?? "") != "password" } - let chosen: MSALNativeAuthHALResponse.EmbeddedMethod? - if parameters.password != nil, let passwordMethod = passwordMethod { - chosen = passwordMethod - } else { - chosen = otpMethod ?? passwordMethod - } - - guard let method = chosen, let href = method.link(for: .challenge) else { - return failure( - .error( - MSALNativeAuthFlowError( - type: .generalError, - errorDescription: "No usable sign-in method returned" - ) - ), - event: event, - context: context, scenario: flowScenario - ) - } - challengeContinuationToken = token - challengeHref = href - challengeHint = method.hint - case .challengeRequired(let token, let href, let hint): - challengeContinuationToken = token - challengeHref = href - challengeHint = hint - default: - return await mapInteraction( - startResult, - flowScenario: flowScenario, - username: parameters.username, - scopes: scopes, - apiId: apiId, - event: event, - context: context - ) - } - - let challengeResult = await performInteraction(context: context) { - try self.requestProvider.challenge(href: challengeHref, continuationToken: challengeContinuationToken, apiId: apiId, context: context) - } - - if let password = parameters.password, case .passwordRequired(let token, let verifyHref) = challengeResult { - let verifyResult = await performInteraction(context: context) { - try self.requestProvider.submitPassword(href: verifyHref, password: password, continuationToken: token, apiId: apiId, context: context) - } - return await mapInteraction( - verifyResult, - flowScenario: flowScenario, - username: parameters.username, - scopes: scopes, - apiId: apiId, - event: event, - context: context - ) - } - - return await mapInteraction( - challengeResult, - flowScenario: flowScenario, - username: parameters.username, - scopes: scopes, - apiId: apiId, - event: event, - context: context, - fallbackHint: challengeHint - ) + return notImplementedResponse(scenario: .signIn) } func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse { @@ -274,82 +126,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MARK: - Continuation - // swiftlint:disable:next function_body_length func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let continuation = state.continuation - - guard let verifyHref = continuation.link(.verify)?.absoluteString else { - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) - return failure( - .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), - event: event, - context: context, scenario: continuation.flowScenario - ) - } - - switch continuation.flowScenario { - case .signIn, .signUp: - let apiId: MSALNativeAuthTelemetryApiId = continuation.flowScenario == .signUp - ? .telemetryApiIdV2SignUpSubmitCode - : .telemetryApiIdV2SignInSubmitCode - let event = makeAndStartTelemetryEvent(id: apiId, context: context) - let result = await performInteraction(context: context) { - try self.requestProvider.verify( - href: verifyHref, - otp: code, - continuationToken: continuation.continuationToken, - apiId: apiId, - context: context - ) - } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: apiId, - event: event, - context: context, - recoverableState: state, - signUpAutofillValues: continuation.signUpAutofillValues, - signUpAutofillSubmittedIds: continuation.signUpAutofillSubmittedIds - ) - case .passwordReset: - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) - let result = await performInteraction(context: context) { - try self.requestProvider.verify( - href: verifyHref, - otp: code, - continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2ResetPasswordSubmitCode, - context: context - ) - } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2ResetPasswordSubmitCode, - event: event, - context: context, - recoverableState: state - ) - case .unknown: - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) - return failure( - .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unknown flow for verify link")), - event: event, - context: context, scenario: continuation.flowScenario - ) - } - } - - func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { - let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignInSubmitPassword, context: context) - let continuation = state.continuation + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) guard let verifyHref = continuation.link(.verify)?.absoluteString else { return failure( @@ -360,11 +140,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } let result = await performInteraction(context: context) { - try self.requestProvider.submitPassword( + try self.requestProvider.verify( href: verifyHref, - password: password, + otp: code, continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2SignInSubmitPassword, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, context: context ) } @@ -373,13 +153,17 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: continuation.flowScenario, username: continuation.username, scopes: continuation.scopes, - apiId: .telemetryApiIdV2SignInSubmitPassword, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, event: event, context: context, recoverableState: state ) } + func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return notImplementedResponse(scenario: state.continuation.flowScenario) + } + // swiftlint:disable:next function_body_length func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) @@ -465,174 +249,19 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { - let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2SignUpSubmitAttributes, context: context) - let continuation = state.continuation - - guard let submitHref = continuation.link(.submitAttributes)?.absoluteString else { - return failure( - .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing submit-attributes link")), - event: event, - context: context, scenario: continuation.flowScenario - ) - } - - let result = await performInteraction(context: context) { - try self.requestProvider.submitAttributes( - href: submitHref, - attributes: attributes, - continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2SignUpSubmitAttributes, - context: context - ) - } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2SignUpSubmitAttributes, - event: event, - context: context, - recoverableState: state, - signUpAutofillValues: continuation.signUpAutofillValues, - signUpAutofillSubmittedIds: continuation.signUpAutofillSubmittedIds - ) + return notImplementedResponse(scenario: state.continuation.flowScenario) } - // swiftlint:disable:next function_body_length func selectAuthMethod( _ method: MSALAuthMethod, verificationContact: String?, state: MSALNativeAuthFlowInternalState ) async -> MSALNativeAuthFlowControllerResponse { - let context = MSALNativeAuthRequestContext(correlationId: nil) - let continuation = state.continuation - - // JIT (strong-auth registration) carries an `enroll` link; MFA carries a `challenge` link. - if continuation.link(.enroll) != nil { - guard let enrollHref = (continuation.methodLink(for: method.id) ?? continuation.link(.enroll))?.absoluteString else { - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) - return failure( - .error( - MSALNativeAuthFlowError( - type: .generalError, - errorDescription: "Missing enroll link for selected method" - ) - ), - event: event, - context: context, scenario: continuation.flowScenario - ) - } - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2JITChallenge, context: context) - let result = await performInteraction(context: context) { - try self.requestProvider.registerMethod( - href: enrollHref, - target: verificationContact, - continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2JITChallenge, - context: context - ) - } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2JITChallenge, - event: event, - context: context - ) - } else { - guard let challengeHref = (continuation.methodLink(for: method.id) ?? continuation.link(.challenge))?.absoluteString else { - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) - return failure( - .error( - MSALNativeAuthFlowError( - type: .generalError, - errorDescription: "Missing challenge link for selected method" - ) - ), - event: event, - context: context, scenario: continuation.flowScenario - ) - } - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFAGetAuthMethods, context: context) - let result = await performInteraction(context: context) { - try self.requestProvider.challenge( - href: challengeHref, - continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2MFAGetAuthMethods, - context: context - ) - } - - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2MFAGetAuthMethods, - event: event, - context: context, - fallbackHint: continuation.sentToHint, - codeRequiredAsMFA: true - ) - } + return notImplementedResponse(scenario: state.continuation.flowScenario) } func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { - let context = MSALNativeAuthRequestContext(correlationId: nil) - let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2MFASubmitChallenge, context: context) - let continuation = state.continuation - - // JIT activation uses the `activate` link and submits the code via the `code` field; - // MFA uses the `verify` link and submits the code via the `otp` field. - let submitHref: String - let isActivation: Bool - if let activateHref = continuation.link(.activate)?.absoluteString { - submitHref = activateHref - isActivation = true - } else if let verifyHref = continuation.link(.verify)?.absoluteString { - submitHref = verifyHref - isActivation = false - } else { - return failure( - .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), - event: event, - context: context, scenario: continuation.flowScenario - ) - } - - let result = await performInteraction(context: context) { - if isActivation { - return try self.requestProvider.submitCode( - href: submitHref, - code: challenge, - continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2MFASubmitChallenge, - context: context - ) - } else { - return try self.requestProvider.verify( - href: submitHref, - otp: challenge, - continuationToken: continuation.continuationToken, - apiId: .telemetryApiIdV2MFASubmitChallenge, - context: context - ) - } - } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2MFASubmitChallenge, - event: event, - context: context, - recoverableState: state - ) + return notImplementedResponse(scenario: state.continuation.flowScenario) } func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { @@ -717,10 +346,8 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MARK: - Result mapping - // Maps a validated interaction response onto a controller response (the unified, server-driven - // branch used by sign in / sign up / MFA / JIT continuation steps). On a terminal `continue` - // state it runs the completion (authorize-challenge → token) sequence. - // swiftlint:disable:next function_body_length cyclomatic_complexity + // Maps a validated interaction response onto a controller response, building the next + // required state or, on a terminal response, running the authorize-challenge → token completion. private func mapInteraction( _ result: MSALNativeAuthV2InteractionValidatedResponse, flowScenario: MSALNativeAuthFlowScenario, @@ -730,10 +357,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, recoverableState: MSALNativeAuthFlowInternalState? = nil, - fallbackHint: String? = nil, - codeRequiredAsMFA: Bool = false, - signUpAutofillValues: [String: Any]? = nil, - signUpAutofillSubmittedIds: Set = [] + fallbackHint: String? = nil ) async -> MSALNativeAuthFlowControllerResponse { switch result { case .readyToComplete(let token): @@ -754,150 +378,24 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati username: username, sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, codeLength: codeLength, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds + scopes: scopes ) let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo - let state: MSALNativeAuthState = codeRequiredAsMFA - ? MSALNativeAuthMFAVerificationRequiredState(internalState: newState, sentTo: displaySentTo, channel: channelType, codeLength: codeLength) - : MSALNativeAuthCodeRequiredState(internalState: newState, sentTo: displaySentTo, channel: channelType, codeLength: codeLength) + let state = MSALNativeAuthCodeRequiredState(internalState: newState, sentTo: displaySentTo, channel: channelType, codeLength: codeLength) stopTelemetryEvent(event, context: context) return response(.actionRequired(state: state), context: context) - case .passwordRequired(let token, let verifyHref): - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.verify: verifyHref], - username: username, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds - ) - stopTelemetryEvent(event, context: context) - return response( - .actionRequired(state: MSALNativeAuthPasswordRequiredState(internalState: newState)), - context: context) case .updateRequired(let token, let updateHref): let newState = makeState( flowScenario, continuationToken: token, links: [.update: updateHref], username: username, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds + scopes: scopes ) stopTelemetryEvent(event, context: context) return response( .actionRequired(state: MSALNativeAuthNewPasswordRequiredState(internalState: newState)), context: context) - case .attributesRequired(let token, let attributes, let submitHref): - // Sign-up: submit values the app supplied at start (e.g. email/password) automatically. - // The full set is kept intact for the whole flow; only the attributes the server - // requests in this step are sent, so the app never sees them. - if flowScenario == .signUp, - let autoValues = autoAttributeValues(for: attributes, from: signUpAutofillValues) { - let autoIds = Set(autoValues.keys) - // If the server re-requests an attribute we already auto-submitted - // we throw an error specifying this - let repeats = autoIds.intersection(signUpAutofillSubmittedIds) - if !repeats.isEmpty { - let repeatedIds = repeats.sorted().joined(separator: ", ") - return failure( - .error(MSALNativeAuthFlowError( - type: .generalError, - errorDescription: "The server re-requested attribute(s) already submitted: \(repeatedIds)." - )), - event: event, - context: context, scenario: flowScenario - ) - } - let submitState = makeState( - flowScenario, - continuationToken: token, - links: [.submitAttributes: submitHref], - username: username, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds.union(autoIds) - ) - stopTelemetryEvent(event, context: context) - return await submitAttributes(autoValues, state: submitState) - } - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.submitAttributes: submitHref], - username: username, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds - ) - stopTelemetryEvent(event, context: context) - return response( - .actionRequired( - state: MSALNativeAuthAttributesRequiredState(internalState: newState, attributes: requiredAttributes(from: attributes)) - ), - context: context) - case .mfaRequired(let token, let methods, let challengeHref): - let (authMethods, methodLinks) = authMethods(from: methods) - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.challenge: challengeHref], - username: username, - authMethods: authMethods, - methodLinks: methodLinks, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds - ) - stopTelemetryEvent(event, context: context) - return response( - .actionRequired(state: MSALNativeAuthMFARequiredState(internalState: newState, authMethods: authMethods)), - context: context) - case .registrationRequired(let token, let enrollHref, let methods): - let (authMethods, methodLinks) = authMethods(from: methods) - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.enroll: enrollHref], - username: username, - authMethods: authMethods, - methodLinks: methodLinks, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds - ) - stopTelemetryEvent(event, context: context) - return response( - .actionRequired(state: MSALNativeAuthStrongAuthRegistrationRequiredState(internalState: newState, authMethods: authMethods)), - context: context) - case .activationRequired(let token, let activateHref, let sentTo, let channelType, let codeLength): - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.activate: activateHref], - username: username, - sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, - codeLength: codeLength, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds - ) - let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo - stopTelemetryEvent(event, context: context) - return response( - .actionRequired( - state: MSALNativeAuthStrongAuthVerificationRequiredState( - internalState: newState, - sentTo: displaySentTo, - channel: channelType, - codeLength: codeLength - ) - ), - context: context) case .error(let error): stopTelemetryEvent(event, context: context, error: error) return response( @@ -1022,11 +520,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati username: String?, sentToHint: String? = nil, codeLength: Int? = nil, - authMethods: [MSALAuthMethod] = [], - methodLinks: [String: String] = [:], - scopes: [String] = [], - signUpAutofillValues: [String: Any]? = nil, - signUpAutofillSubmittedIds: Set = [] + scopes: [String] = [] ) -> MSALNativeAuthFlowInternalState { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) var resolvedLinks: [MSALNativeAuthV2LinkKey: URL] = [:] @@ -1035,11 +529,6 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati resolvedLinks[.relation(relation)] = url } } - for (methodId, href) in methodLinks { - if let url = try? resolver.url(forHref: href) { - resolvedLinks[.method(id: methodId)] = url - } - } let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, continuationToken: continuationToken, @@ -1047,71 +536,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati username: username, sentToHint: sentToHint, codeLength: codeLength, - authMethods: authMethods, - scopes: scopes, - signUpAutofillValues: signUpAutofillValues, - signUpAutofillSubmittedIds: signUpAutofillSubmittedIds + scopes: scopes ) return MSALNativeAuthFlowInternalState(continuation: continuation, controller: self) } - /// Converts embedded HAL methods into public ``MSALAuthMethod`` objects plus a map of each - /// method's action href (keyed by method id) for later selection. MFA methods carry a - /// `challenge` link; JIT registration methods carry an `enroll`/`register` link. - private func authMethods( - from methods: [MSALNativeAuthHALResponse.EmbeddedMethod] - ) -> (methods: [MSALAuthMethod], methodLinks: [String: String]) { - var out: [MSALAuthMethod] = [] - var methodLinks: [String: String] = [:] - for method in methods { - let id = method.id ?? "" - let type = method.type ?? "" - out.append(MSALAuthMethod( - id: id, - challengeType: type, - channelTargetType: MSALNativeAuthChannelType(value: type), - loginHint: method.hint - )) - if let link = method.link(for: .challenge) ?? method.link(for: .enroll) ?? method.link(for: .register) { - methodLinks[id] = link - } - } - return (out, methodLinks) - } - - private func requiredAttributes( - from attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry] - ) -> [MSALNativeAuthRequiredAttribute] { - return attributes.map { entry in - MSALNativeAuthRequiredAttribute( - name: entry.id ?? "", - type: entry.type ?? "", - required: entry.required, - regex: entry.regex - ) - } - } - - /// Returns the values needed to satisfy a `collectAttributes` request from the values the app - /// supplied at sign-up start, but only when *every* required attribute is covered. - /// Optional attributes are never auto-submitted - private func autoAttributeValues( - for requested: [MSALNativeAuthHALResponse.RequiredAttributeEntry], - from autofill: [String: Any]? - ) -> [String: Any]? { - guard let autofill = autofill, !requested.isEmpty else { - return nil - } - var values: [String: Any] = [:] - for entry in requested where entry.required { - guard let id = entry.id, let value = autofill[id] else { - return nil - } - values[id] = value - } - return values.isEmpty ? nil : values - } - // MARK: - Response construction private func response( @@ -1138,6 +567,15 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } + /// Response for flows that are not implemented currently + private func notImplementedResponse(scenario: MSALNativeAuthFlowScenario) -> MSALNativeAuthFlowControllerResponse { + return MSALNativeAuthFlowControllerResponse( + .error(error: MSALNativeAuthFlowError(type: .notImplemented), newState: nil), + correlationId: UUID(), + scenario: scenario + ) + } + private func failure( _ validated: MSALNativeAuthV2AuthorizeChallengeValidatedResponse, event: MSIDTelemetryAPIEvent?, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift index f6ac38f61a..3c9c105d26 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift @@ -65,38 +65,10 @@ struct MSALNativeAuthFlowResponseDispatcher { await deliver(to: delegate, response: response, as: MSALNativeAuthCodeRequiredDelegate.self, scenario: scenario) { await $0.onCodeRequired(state: state, scenario: scenario) } - case let state as MSALNativeAuthPasswordRequiredState: - await deliver(to: delegate, response: response, as: MSALNativeAuthPasswordRequiredDelegate.self, scenario: scenario) { - await $0.onPasswordRequired(state: state, scenario: scenario) - } case let state as MSALNativeAuthNewPasswordRequiredState: await deliver(to: delegate, response: response, as: MSALNativeAuthNewPasswordRequiredDelegate.self, scenario: scenario) { await $0.onNewPasswordRequired(state: state, scenario: scenario) } - case let state as MSALNativeAuthAttributesRequiredState: - await deliver(to: delegate, response: response, as: MSALNativeAuthAttributesRequiredDelegate.self, scenario: scenario) { - await $0.onAttributesRequired(state: state, scenario: scenario) - } - case let state as MSALNativeAuthAttributesInvalidState: - await deliver(to: delegate, response: response, as: MSALNativeAuthAttributesInvalidDelegate.self, scenario: scenario) { - await $0.onAttributesInvalid(state: state, scenario: scenario) - } - case let state as MSALNativeAuthMFARequiredState: - await deliver(to: delegate, response: response, as: MSALNativeAuthMFARequiredDelegate.self, scenario: scenario) { - await $0.onMFARequired(state: state, scenario: scenario) - } - case let state as MSALNativeAuthMFAVerificationRequiredState: - await deliver(to: delegate, response: response, as: MSALNativeAuthMFAVerificationRequiredDelegate.self, scenario: scenario) { - await $0.onMFAVerificationRequired(state: state, scenario: scenario) - } - case let state as MSALNativeAuthStrongAuthRegistrationRequiredState: - await deliver(to: delegate, response: response, as: MSALNativeAuthStrongAuthRegistrationRequiredDelegate.self, scenario: scenario) { - await $0.onStrongAuthRegistrationRequired(state: state, scenario: scenario) - } - case let state as MSALNativeAuthStrongAuthVerificationRequiredState: - await deliver(to: delegate, response: response, as: MSALNativeAuthStrongAuthVerificationRequiredDelegate.self, scenario: scenario) { - await $0.onStrongAuthVerificationRequired(state: state, scenario: scenario) - } default: await notImplemented(delegate: delegate, scenario: scenario, correlationId: response.correlationId) } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index 82b16c076d..a13b44ef4e 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -75,7 +75,6 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin } } - // swiftlint:disable:next cyclomatic_complexity function_body_length func validateInteraction( context: MSIDRequestContext, _ result: Result @@ -113,74 +112,23 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin guard let challengeHref = method?.link(for: .challenge) ?? response.href(for: .challenge) else { return missingLink(.challenge, context: context) } - // MFA required: the server sets challengeContext.authenticationFactor to "multiFactor" - // and embeds the available second-factor methods. Surface them for method selection - // rather than auto-triggering a single challenge. - if response.authenticationFactor == "multiFactor", !response.methods.isEmpty { - return .mfaRequired( - continuationToken: continuationToken, - methods: response.methods, - challengeHref: challengeHref - ) - } return .challengeRequired( continuationToken: continuationToken, challengeHref: challengeHref, hint: method?.hint ?? response.hint ) case .verify: - // After a password, a `challenge` link plus embedded methods means MFA is required. - if let challengeHref = response.href(for: .challenge), !response.methods.isEmpty { - return .mfaRequired( - continuationToken: continuationToken, - methods: response.methods, - challengeHref: challengeHref - ) - } guard let verifyHref = response.href(for: .verify) else { return missingLink(.verify, context: context) } - // An email/OOB method carries a hint and/or a code length; a password method does not. - if (response.codeLength ?? 0) > 0 || response.hint != nil || response.methodType == "email" { - return .codeRequired( - continuationToken: continuationToken, - verifyHref: verifyHref, - resendHref: response.href(for: .resend), - sentTo: response.hint ?? "", - channelType: MSALNativeAuthChannelType(value: response.methodType ?? "email"), - codeLength: response.codeLength ?? 0 - ) - } - return .passwordRequired(continuationToken: continuationToken, verifyHref: verifyHref) - case .enroll, .register: - guard let enrollHref = response.href(for: .enroll) ?? response.href(for: .register) else { - return missingLink(.enroll, context: context) - } - return .registrationRequired( - continuationToken: continuationToken, - enrollHref: enrollHref, - methods: response.methods - ) - case .activate: - guard let activateHref = response.href(for: .activate) else { - return missingLink(.activate, context: context) - } - return .activationRequired( + return .codeRequired( continuationToken: continuationToken, - activateHref: activateHref, + verifyHref: verifyHref, + resendHref: response.href(for: .resend), sentTo: response.hint ?? "", channelType: MSALNativeAuthChannelType(value: response.methodType ?? "email"), codeLength: response.codeLength ?? 0 ) - case .collectAttributes: - guard let submitHref = response.href(for: .submitAttributes) else { - return missingLink(.submitAttributes, context: context) - } - return .attributesRequired( - continuationToken: continuationToken, - attributes: response.attributes, - submitHref: submitHref - ) case .update: guard let updateHref = response.href(for: .update) ?? response.href(for: .self) else { return missingLink(.update, context: context) @@ -198,15 +146,6 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin pollHref: pollHref ) default: - // No recognized action. A nil action with embedded methods is sign-in method discovery. - if response.action == nil, !response.methods.isEmpty { - MSALNativeAuthLogger.log( - level: .verbose, - context: context, - format: "interaction: returning %d sign-in methods", - response.methods.count) - return .signInMethods(continuationToken: continuationToken, methods: response.methods) - } MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: unexpected action '%@'", response.action ?? "nil") return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index 97f9ecbd61..0efe40a62b 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -52,23 +52,10 @@ enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { /// A single enum represents every HAL interaction response; the validator selects the case /// from the HAL `state` / `action` pair. enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { - /// Sign-in method discovery: the `signin` response carries a continuation token and the - /// available authentication methods (each with its own `challenge` link). - case signInMethods(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) /// `action == challenge`: a verification method is available; the SDK should auto-trigger the challenge. case challengeRequired(continuationToken: String, challengeHref: String, hint: String?) - /// `action == verify` on a password method: the user must enter their password. - case passwordRequired(continuationToken: String, verifyHref: String) /// `action == verify`: a one-time code is required from the user. case codeRequired(continuationToken: String, verifyHref: String, resendHref: String?, sentTo: String, channelType: MSALNativeAuthChannelType, codeLength: Int) - /// `action == verify` after a password, carrying a `challenge` link and the MFA methods. - case mfaRequired(continuationToken: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod], challengeHref: String) - /// `action == enroll`/`register`: strong-auth (JIT) registration is required; pick a method to enroll. - case registrationRequired(continuationToken: String, enrollHref: String, methods: [MSALNativeAuthHALResponse.EmbeddedMethod]) - /// `action == activate`: a JIT enrollment code is required from the user. - case activationRequired(continuationToken: String, activateHref: String, sentTo: String, channelType: MSALNativeAuthChannelType, codeLength: Int) - /// `action == collectAttributes`: sign-up attributes are required from the user. - case attributesRequired(continuationToken: String, attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry], submitHref: String) /// `action == update`: a new password is required from the user. case updateRequired(continuationToken: String, updateHref: String) /// `action == poll`: the operation is still running; keep polling. @@ -77,25 +64,12 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { case readyToComplete(continuationToken: String) case error(MSALNativeAuthFlowError) - // swiftlint:disable:next cyclomatic_complexity static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { - case let (.signInMethods(lToken, lMethods), .signInMethods(rToken, rMethods)): - return lToken == rToken && lMethods == rMethods case let (.challengeRequired(lToken, lHref, lHint), .challengeRequired(rToken, rHref, rHint)): return lToken == rToken && lHref == rHref && lHint == rHint - case let (.passwordRequired(lToken, lHref), .passwordRequired(rToken, rHref)): - return lToken == rToken && lHref == rHref case let (.codeRequired(lToken, lVerify, lResend, lSent, lChannel, lLen), .codeRequired(rToken, rVerify, rResend, rSent, rChannel, rLen)): return lToken == rToken && lVerify == rVerify && lResend == rResend && lSent == rSent && lChannel.value == rChannel.value && lLen == rLen - case let (.mfaRequired(lToken, lMethods, lHref), .mfaRequired(rToken, rMethods, rHref)): - return lToken == rToken && lMethods == rMethods && lHref == rHref - case let (.registrationRequired(lToken, lHref, lMethods), .registrationRequired(rToken, rHref, rMethods)): - return lToken == rToken && lHref == rHref && lMethods == rMethods - case let (.activationRequired(lToken, lHref, lSent, lChannel, lLen), .activationRequired(rToken, rHref, rSent, rChannel, rLen)): - return lToken == rToken && lHref == rHref && lSent == rSent && lChannel.value == rChannel.value && lLen == rLen - case let (.attributesRequired(lToken, lAttrs, lHref), .attributesRequired(rToken, rAttrs, rHref)): - return lToken == rToken && lAttrs == rAttrs && lHref == rHref case let (.updateRequired(lToken, lHref), .updateRequired(rToken, rHref)): return lToken == rToken && lHref == rHref case let (.pollInProgress(lToken, lHref), .pollInProgress(rToken, rHref)): diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift index 55ce8572a1..ac4507bd5b 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift @@ -29,34 +29,18 @@ import Foundation /// fields the operation carries struct MSALNativeAuthV2RequestBody { let continuationToken: String - var password: String? - var code: String? var otp: String? var newPassword: String? - var target: String? - var attributes: [String: Any]? var dictionary: [String: Any] { var body: [String: Any] = [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] - if let password = password { - body[MSALNativeAuthV2RequestBodyKey.password.rawValue] = password - } - if let code = code { - body[MSALNativeAuthV2RequestBodyKey.code.rawValue] = code - } if let otp = otp { body[MSALNativeAuthV2RequestBodyKey.otp.rawValue] = otp } if let newPassword = newPassword { body[MSALNativeAuthV2RequestBodyKey.newPassword.rawValue] = newPassword } - if let target = target { - body[MSALNativeAuthV2RequestBodyKey.target.rawValue] = target - } - if let attributes = attributes { - body[MSALNativeAuthV2RequestBodyKey.attributes.rawValue] = attributes - } return body } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift index 0f58ae68cb..f1110a050b 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift @@ -32,10 +32,7 @@ import Foundation enum MSALNativeAuthV2RequestBodyKey: String { case username case continuationToken - case password case code case otp case newPassword - case attributes - case target } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 51e17c9eb5..942d651f47 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -26,22 +26,6 @@ protocol MSALNativeAuthV2RequestProviding { - /// Sign up entry: posts the username to the authorize-challenge `sign_up` href. - func signUpStart(username: String, - continuationToken: String, - href: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest - - /// Sign in entry: posts the username to the authorize-challenge `sign_in` href. - func signInStart(username: String, - continuationToken: String, - href: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest - /// SSPR entry, posted to the authorize-challenge `reset_password` href. func resetPasswordStart(username: String, continuationToken: String, @@ -50,38 +34,6 @@ protocol MSALNativeAuthV2RequestProviding { context: MSALNativeAuthRequestContext ) throws -> MSIDHttpRequest - /// Submit a password to a server `verify` href (sign in / MFA primary factor). - func submitPassword(href: String, - password: String, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest - - /// Submit a one-time `code` to a server `verify` / `activate` href (sign in / sign up / MFA / JIT). - func submitCode(href: String, - code: String, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest - - /// Submit collected attributes (sign up) to a server `submitAttributes` href. - func submitAttributes(href: String, - attributes: [String: Any], - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest - - /// Register a strong-auth method (JIT) by posting the target to a server `enroll` href. - func registerMethod(href: String, - target: String?, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest - /// Send EOTP (server `challenge` / `resend` href). func challenge(href: String, continuationToken: String, @@ -141,38 +93,6 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { self.configurator = MSALNativeAuthV2RequestConfigurator(config: config) } - func signUpStart(username: String, - continuationToken: String, - href: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( - context: context, - target: .href(href), - apiId: apiId, - operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, - username: username, - continuationToken: continuationToken - )) - } - - func signInStart(username: String, - continuationToken: String, - href: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2EntryParameters( - context: context, - target: .href(href), - apiId: apiId, - operationType: MSALNativeAuthV2OperationType.signInStart.rawValue, - username: username, - continuationToken: continuationToken - )) - } - func resetPasswordStart(username: String, continuationToken: String, href: String, @@ -189,70 +109,6 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { )) } - func submitPassword(href: String, - password: String, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( - context: context, - href: href, - httpMethod: "POST", - apiId: apiId, - operationType: MSALNativeAuthV2OperationType.submitPassword.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, password: password) - )) - } - - func submitCode(href: String, - code: String, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( - context: context, - href: href, - httpMethod: "POST", - apiId: apiId, - operationType: MSALNativeAuthV2OperationType.submitCode.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, code: code) - )) - } - - func submitAttributes(href: String, - attributes: [String: Any], - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( - context: context, - href: href, - httpMethod: "POST", - apiId: apiId, - operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, attributes: attributes) - )) - } - - func registerMethod(href: String, - target: String?, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - return try configurator.configure(parameters: MSALNativeAuthV2HrefParameters( - context: context, - href: href, - httpMethod: "POST", - apiId: apiId, - operationType: MSALNativeAuthV2OperationType.registerMethod.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, target: target) - )) - } - func challenge(href: String, continuationToken: String, apiId: MSALNativeAuthTelemetryApiId, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 19c99c5203..64f711d934 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -184,40 +184,6 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { XCTAssertFalse(requestProviderMock.verifyCalled) } - func test_submitCode_signIn_usesVerifyOtpAndNotSubmitCode() async { - requestProviderMock.mockRequest() - validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] - validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/verify")!]) - - let response = await sut.submitCode("12345678", state: state) - - guard case .completed = response.result else { - return XCTFail("Expected completed, got \(response.result)") - } - // The `/auth/methods/{type}/{id}/verify` endpoint expects the code in the `otp` field. - XCTAssertTrue(requestProviderMock.verifyCalled) - XCTAssertFalse(requestProviderMock.submitCodeCalled) - } - - func test_submitCode_signUp_usesVerifyOtpAndNotSubmitCode() async { - requestProviderMock.mockRequest() - validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] - validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - let state = makeState(flowScenario: .signUp, links: ["verify": URL(string: "https://contoso.com/verify")!]) - - let response = await sut.submitCode("12345678", state: state) - - guard case .completed = response.result else { - return XCTFail("Expected completed, got \(response.result)") - } - // The `/auth/methods/{type}/{id}/verify` endpoint expects the code in the `otp` field. - XCTAssertTrue(requestProviderMock.verifyCalled) - XCTAssertFalse(requestProviderMock.submitCodeCalled) - } - // MARK: - submitNewPassword (poll -> token -> completed) func test_submitNewPassword_happyPath_returnsCompleted() async { @@ -273,224 +239,4 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { } XCTAssertTrue(requestProviderMock.challengeCalled) } - - // MARK: - signUp / signIn / MFA / JIT - - private func makeState( - flowScenario: MSALNativeAuthFlowScenario, - links: [String: URL], - authMethods: [MSALAuthMethod] = [], - continuationToken: String = "ct" - ) -> MSALNativeAuthFlowInternalState { - let continuation = MSALNativeAuthFlowContinuationState( - flowScenario: flowScenario, - continuationToken: continuationToken, - links: relationLinks(links), - username: "user@contoso.com", - sentToHint: "u***@contoso.com", - codeLength: 8, - authMethods: authMethods - ) - return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) - } - - func test_signUp_happyPath_returnsCodeRequired() async { - requestProviderMock.mockRequest() - validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup") - ] - validatorMock.interactionResponses = [ - .codeRequired(continuationToken: "ct-2", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) - ] - - let response = await sut.signUp(parameters: MSALNativeAuthSignUpParametersV2(username: "user@contoso.com")) - - guard case .actionRequired(let state) = response.result, state is MSALNativeAuthCodeRequiredState else { - return XCTFail("Expected codeRequired state, got \(response.result)") - } - XCTAssertTrue(requestProviderMock.signUpStartCalled) - } - - func test_signUp_autoSubmitsSuppliedEmailAndPassword() async { - requestProviderMock.mockRequest() - let emailAttribute = MSALNativeAuthHALResponse.RequiredAttributeEntry(id: "email", type: "text", required: true, regex: nil) - let passwordAttribute = MSALNativeAuthHALResponse.RequiredAttributeEntry(id: "password", type: "password", required: true, regex: nil) - validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup"), - .authorizationCode(code: "auth-code") - ] - validatorMock.interactionResponses = [ - .attributesRequired(continuationToken: "ct-email", attributes: [emailAttribute], submitHref: "https://contoso.com/submit"), - .codeRequired(continuationToken: "ct-code", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8), - .attributesRequired(continuationToken: "ct-pwd", attributes: [passwordAttribute], submitHref: "https://contoso.com/submit"), - .readyToComplete(continuationToken: "ct-continue") - ] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - - let parameters = MSALNativeAuthSignUpParametersV2(username: "user@contoso.com") - parameters.password = "Secret-Password-1" - - // Step 1: sign-up start. The server asks for `email`; the SDK auto-submits it and the app - // only sees the subsequent codeRequired action - never an attributesRequired for email. - let startResponse = await sut.signUp(parameters: parameters) - guard case .actionRequired(let codeState) = startResponse.result, codeState is MSALNativeAuthCodeRequiredState else { - return XCTFail("Expected codeRequired state, got \(startResponse.result)") - } - - // Step 2: submit the email code. The server then asks for `password`; the SDK auto-submits - // the originally supplied password and completes without surfacing attributesRequired. - let finalResponse = await sut.submitCode("12345678", state: codeState.internalState) - guard case .completed = finalResponse.result else { - return XCTFail("Expected completed, got \(finalResponse.result)") - } - - // Email and password were submitted automatically as collected attributes. - XCTAssertTrue(requestProviderMock.submitAttributesCalled) - XCTAssertEqual(requestProviderMock.submitAttributesHistory.count, 2) - XCTAssertEqual(requestProviderMock.submitAttributesHistory.first?["email"] as? String, "user@contoso.com") - XCTAssertEqual(requestProviderMock.submitAttributesHistory.last?["password"] as? String, "Secret-Password-1") - XCTAssertTrue(requestProviderMock.verifyCalled) - XCTAssertTrue(requestProviderMock.tokenCalled) - } - - func test_signUp_whenServerRerequestsAlreadySubmittedAttribute_returnsError() async { - requestProviderMock.mockRequest() - let emailAttribute = MSALNativeAuthHALResponse.RequiredAttributeEntry(id: "email", type: "text", required: true, regex: nil) - validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signup") - ] - validatorMock.interactionResponses = [ - .attributesRequired(continuationToken: "ct-email", attributes: [emailAttribute], submitHref: "https://contoso.com/submit"), - .attributesRequired(continuationToken: "ct-email-2", attributes: [emailAttribute], submitHref: "https://contoso.com/submit") - ] - - let parameters = MSALNativeAuthSignUpParametersV2(username: "user@contoso.com") - parameters.password = "Secret-Password-1" - - let response = await sut.signUp(parameters: parameters) - - guard case .error(let error, _) = response.result else { - return XCTFail("Expected error, got \(response.result)") - } - // Email was auto-submitted exactly once; the re-request produced an error rather than a loop. - XCTAssertEqual(requestProviderMock.submitAttributesHistory.count, 1) - XCTAssertEqual(requestProviderMock.submitAttributesHistory.first?["email"] as? String, "user@contoso.com") - // The error names the repeated attribute id but never the value. - XCTAssertTrue(error.errorDescription?.contains("email") ?? false) - XCTAssertFalse(error.errorDescription?.contains("user@contoso.com") ?? true) - XCTAssertFalse(error.errorDescription?.contains("Secret-Password-1") ?? true) - } - - func test_signIn_withPassword_happyPath_returnsCompleted() async { - requestProviderMock.mockRequest() - let passwordMethod = MSALNativeAuthHALResponse.EmbeddedMethod( - id: "1", type: "password", hint: nil, links: ["challenge": "https://contoso.com/pw/challenge"]) - validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signin"), - .authorizationCode(code: "auth-code") - ] - validatorMock.interactionResponses = [ - .signInMethods(continuationToken: "ct-2", methods: [passwordMethod]), - .passwordRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/pw/verify"), - .readyToComplete(continuationToken: "ct-4") - ] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - - let params = MSALNativeAuthSignInParameters(username: "user@contoso.com") - params.password = "password" - let response = await sut.signIn(parameters: params) - - guard case .completed = response.result else { - return XCTFail("Expected completed, got \(response.result)") - } - XCTAssertTrue(requestProviderMock.signInStartCalled) - XCTAssertTrue(requestProviderMock.submitPasswordCalled) - XCTAssertTrue(requestProviderMock.tokenCalled) - } - - func test_signIn_withCode_returnsCodeRequired() async { - requestProviderMock.mockRequest() - let emailMethod = MSALNativeAuthHALResponse.EmbeddedMethod( - id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/email/challenge"]) - validatorMock.authorizeChallengeResponses = [ - .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/signin") - ] - validatorMock.interactionResponses = [ - .signInMethods(continuationToken: "ct-2", methods: [emailMethod]), - .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) - ] - - let response = await sut.signIn(parameters: MSALNativeAuthSignInParameters(username: "user@contoso.com")) - - guard case .actionRequired(let state) = response.result, state is MSALNativeAuthCodeRequiredState else { - return XCTFail("Expected codeRequired state, got \(response.result)") - } - } - - func test_submitPassword_whenMFARequired_returnsMFARequired() async { - requestProviderMock.mockRequest() - let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: [:]) - validatorMock.interactionResponses = [ - .mfaRequired(continuationToken: "ct-mfa", methods: [method], challengeHref: "https://contoso.com/mfa/challenge") - ] - let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/pw/verify")!]) - - let response = await sut.submitPassword("password", state: state) - - guard case .actionRequired(let state) = response.result, state is MSALNativeAuthMFARequiredState else { - return XCTFail("Expected mfaRequired state, got \(response.result)") - } - XCTAssertTrue(requestProviderMock.submitPasswordCalled) - } - - func test_submitAttributes_happyPath_returnsCompleted() async { - requestProviderMock.mockRequest() - validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] - validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - let state = makeState(flowScenario: .signUp, links: ["submitAttributes": URL(string: "https://contoso.com/submitattributes")!]) - - let response = await sut.submitAttributes(["displayName": "User"], state: state) - - guard case .completed = response.result else { - return XCTFail("Expected completed, got \(response.result)") - } - XCTAssertTrue(requestProviderMock.submitAttributesCalled) - } - - func test_submitChallenge_happyPath_returnsCompleted() async { - requestProviderMock.mockRequest() - validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] - validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - let state = makeState(flowScenario: .signIn, links: ["verify": URL(string: "https://contoso.com/mfa/verify")!]) - - let response = await sut.submitChallenge("12345678", state: state) - - guard case .completed = response.result else { - return XCTFail("Expected completed, got \(response.result)") - } - // MFA uses the `verify` link and submits the code via the `otp` field. - XCTAssertTrue(requestProviderMock.verifyCalled) - XCTAssertFalse(requestProviderMock.submitCodeCalled) - XCTAssertTrue(requestProviderMock.tokenCalled) - } - - func test_submitChallenge_whenActivateLink_usesSubmitCode() async { - requestProviderMock.mockRequest() - validatorMock.interactionResponses = [.readyToComplete(continuationToken: "ct-continue")] - validatorMock.authorizeChallengeResponses = [.authorizationCode(code: "auth-code")] - cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - let state = makeState(flowScenario: .signIn, links: ["activate": URL(string: "https://contoso.com/jit/activate")!]) - - let response = await sut.submitChallenge("12345678", state: state) - - guard case .completed = response.result else { - return XCTFail("Expected completed, got \(response.result)") - } - // JIT activation uses the `activate` link and submits the code via the `code` field. - XCTAssertTrue(requestProviderMock.submitCodeCalled) - XCTAssertFalse(requestProviderMock.verifyCalled) - XCTAssertTrue(requestProviderMock.tokenCalled) - } } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift index 3f515ad55f..3e8e5dbbae 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift @@ -142,24 +142,6 @@ final class MSALNativeAuthFlowResponseDispatcherTests: XCTestCase { XCTAssertFalse(telemetryFired) } - // MARK: - actionRequired: routes each concrete state to its own callback - - func test_dispatch_actionRequired_passwordRequiredState_routesToPasswordCallback() async { - let delegate = PasswordRequiredDelegateSpy() - let state = MSALNativeAuthPasswordRequiredState(internalState: makeInternalState(scenario: .signIn)) - let response = MSALNativeAuthFlowControllerResponse( - .actionRequired(state: state), - correlationId: UUID(), - scenario: .unknown - ) - - await sut.dispatch(response, delegate: delegate) - - XCTAssertTrue(delegate.passwordRequiredState === state) - XCTAssertEqual(delegate.passwordRequiredScenario, .signIn) - XCTAssertNil(delegate.error) - } - // MARK: - Helpers private func makeInternalState(scenario: MSALNativeAuthFlowScenario = .signIn) -> MSALNativeAuthFlowInternalState { @@ -211,14 +193,3 @@ private final class CodeRequiredDelegateSpy: BaseDelegateSpy, MSALNativeAuthCode codeRequiredScenario = scenario } } - -private final class PasswordRequiredDelegateSpy: BaseDelegateSpy, MSALNativeAuthPasswordRequiredDelegate { - - var passwordRequiredState: MSALNativeAuthPasswordRequiredState? - var passwordRequiredScenario: MSALNativeAuthFlowScenario? - - func onPasswordRequired(state: MSALNativeAuthPasswordRequiredState, scenario: MSALNativeAuthFlowScenario) { - passwordRequiredState = state - passwordRequiredScenario = scenario - } -} diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift index 4e521d2fac..d2d0b39323 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -35,12 +35,6 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { private(set) var tokenCalled = false private(set) var tokenScopes: [String]? private(set) var resetPasswordStartCalled = false - private(set) var signInStartCalled = false - private(set) var signUpStartCalled = false - private(set) var submitPasswordCalled = false - private(set) var submitCodeCalled = false - private(set) var submitAttributesCalled = false - private(set) var registerMethodCalled = false private(set) var challengeCalled = false private(set) var verifyCalled = false private(set) var updatePasswordCalled = false @@ -48,12 +42,6 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { private(set) var challengeHrefReceived: String? private(set) var verifyHrefReceived: String? - private(set) var submitPasswordHrefReceived: String? - private(set) var submitCodeHrefReceived: String? - private(set) var submitAttributesHrefReceived: String? - private(set) var submitAttributesReceived: [String: Any]? - private(set) var submitAttributesHistory: [[String: Any]] = [] - private(set) var registerMethodHrefReceived: String? private(set) var updateHrefReceived: String? private(set) var pollHrefReceived: String? @@ -117,78 +105,6 @@ class MSALNativeAuthV2RequestProviderMock: MSALNativeAuthV2RequestProviding { return try resolveRequest() } - func signInStart( - username: String, - continuationToken: String, - href: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - signInStartCalled = true - return try resolveRequest() - } - - func signUpStart( - username: String, - continuationToken: String, - href: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - signUpStartCalled = true - return try resolveRequest() - } - - func submitPassword( - href: String, - password: String, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - submitPasswordCalled = true - submitPasswordHrefReceived = href - return try resolveRequest() - } - - func submitCode( - href: String, - code: String, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - submitCodeCalled = true - submitCodeHrefReceived = href - return try resolveRequest() - } - - func submitAttributes( - href: String, - attributes: [String: Any], - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - submitAttributesCalled = true - submitAttributesHrefReceived = href - submitAttributesReceived = attributes - submitAttributesHistory.append(attributes) - return try resolveRequest() - } - - func registerMethod( - href: String, - target: String?, - continuationToken: String, - apiId: MSALNativeAuthTelemetryApiId, - context: MSALNativeAuthRequestContext - ) throws -> MSIDHttpRequest { - registerMethodCalled = true - registerMethodHrefReceived = href - return try resolveRequest() - } - func challenge( href: String, continuationToken: String, diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift index 3339fd3f09..16bdda6238 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift @@ -41,18 +41,18 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { // MARK: - EntryParameters func test_entryParameters_body_url_andMetadata() throws { - let href = "/tenant/api/v0.1/auth/methods/signUp" + let href = "/tenant/api/v0.1/auth/methods/resetPassword" let sut = MSALNativeAuthV2EntryParameters( context: context, target: .href(href), - apiId: .telemetryApiIdV2SignUpStart, - operationType: MSALNativeAuthV2OperationType.signUpStart.rawValue, + apiId: .telemetryApiIdV2ResetPasswordStart, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, username: "user@contoso.com", continuationToken: "CT" ) - XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignUpStart) - XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.signUpStart.rawValue) + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordStart) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.resetPasswordStart.rawValue) XCTAssertEqual(sut.encoding, .json) XCTAssertEqual(sut.httpMethod, "POST") XCTAssertFalse(sut.expectsRawJSONResponse) @@ -64,8 +64,8 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { let sut = MSALNativeAuthV2EntryParameters( context: context, target: .endpoint(.authorizeChallenge), - apiId: .telemetryApiIdV2SignInWithCodeStart, - operationType: MSALNativeAuthV2OperationType.signInStart.rawValue, + apiId: .telemetryApiIdV2ResetPasswordStart, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, username: "user@contoso.com", continuationToken: "CT" ) @@ -81,12 +81,12 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { context: context, href: href, httpMethod: "POST", - apiId: .telemetryApiIdV2SignInSubmitCode, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, operationType: MSALNativeAuthV2OperationType.verify.rawValue, requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", otp: "1234") ) - XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmitCode) XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.verify.rawValue) XCTAssertEqual(sut.encoding, .json) XCTAssertEqual(sut.httpMethod, "POST") @@ -109,30 +109,16 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { XCTAssertEqual(sut.body as? [String: String], ["continuationToken": "CT", "newPassword": "newPass"]) } - func test_hrefParameters_withAttributes_body() throws { - let sut = MSALNativeAuthV2HrefParameters( - context: context, - href: "/tenant/api/v0.1/auth/methods/attributes", - httpMethod: "POST", - apiId: .telemetryApiIdV2SignUpSubmitAttributes, - operationType: MSALNativeAuthV2OperationType.submitAttributes.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", attributes: ["city": "Redmond"]) - ) - - XCTAssertEqual(sut.body["continuationToken"] as? String, "CT") - XCTAssertEqual(sut.body["attributes"] as? [String: String], ["city": "Redmond"]) - } - // MARK: - AuthorizeChallengeStartParameters func test_authorizeChallengeStartParameters_body_url_andMetadata() throws { let sut = MSALNativeAuthV2AuthorizeChallengeStartParameters( context: context, clientId: "client-id", - apiId: .telemetryApiIdV2SignInWithCodeStart + apiId: .telemetryApiIdV2ResetPasswordStart ) - XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInWithCodeStart) + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordStart) XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.authorizeChallengeStart.rawValue) XCTAssertEqual(sut.encoding, .wwwFormUrlEncoded) XCTAssertEqual(sut.httpMethod, "POST") @@ -147,10 +133,10 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { let sut = MSALNativeAuthV2AuthorizeChallengeContinueParameters( context: context, continuationToken: "CT", - apiId: .telemetryApiIdV2SignInSubmitCode + apiId: .telemetryApiIdV2ResetPasswordSubmit ) - XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmit) XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.authorizeChallengeContinue.rawValue) XCTAssertEqual(sut.encoding, .wwwFormUrlEncoded) XCTAssertEqual(sut.body as? [String: String], ["continuation_token": "CT"]) @@ -165,10 +151,10 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { clientId: "client-id", code: "auth-code", scopes: ["scope1", "scope2"], - apiId: .telemetryApiIdV2SignInSubmitCode + apiId: .telemetryApiIdV2ResetPasswordSubmit ) - XCTAssertEqual(sut.apiId, .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmit) XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.token.rawValue) XCTAssertEqual(sut.encoding, .wwwFormUrlEncoded) XCTAssertTrue(sut.expectsRawJSONResponse) @@ -188,7 +174,7 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { clientId: "client-id", code: "auth-code", scopes: [], - apiId: .telemetryApiIdV2SignInSubmitCode + apiId: .telemetryApiIdV2ResetPasswordSubmit ) XCTAssertEqual(sut.body as? [String: String], [ diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift index bb8f8cac5b..e0e90940d8 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift @@ -49,34 +49,6 @@ final class MSALNativeAuthV2RequestProviderTests: XCTestCase { // MARK: - Entry requests - func test_signUpStart_configuresHrefRequestAndThreadsApiId() throws { - let request = try sut.signUpStart( - username: "user@contoso.com", - continuationToken: "CT", - href: href, - apiId: .telemetryApiIdV2SignUpStart, - context: context - ) - - XCTAssertEqual(request.urlRequest?.httpMethod, "POST") - XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignUpStart) - XCTAssertTrue(request.responseSerializer is MSALNativeAuthV2HALResponseSerializer) - } - - func test_signInStart_threadsApiId() throws { - let request = try sut.signInStart( - username: "user@contoso.com", - continuationToken: "CT", - href: href, - apiId: .telemetryApiIdV2SignInWithCodeStart, - context: context - ) - - XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInWithCodeStart) - } - func test_resetPasswordStart_threadsApiId() throws { let request = try sut.resetPasswordStart( username: "user@contoso.com", @@ -86,65 +58,14 @@ final class MSALNativeAuthV2RequestProviderTests: XCTestCase { context: context ) + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordStart) + XCTAssertTrue(request.responseSerializer is MSALNativeAuthV2HALResponseSerializer) } // MARK: - HAL follow-up requests - func test_submitPassword_threadsApiId() throws { - let request = try sut.submitPassword( - href: href, - password: "pass", - continuationToken: "CT", - apiId: .telemetryApiIdV2SignInSubmitPassword, - context: context - ) - - XCTAssertEqual(request.urlRequest?.httpMethod, "POST") - XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInSubmitPassword) - } - - func test_submitCode_threadsApiId() throws { - let request = try sut.submitCode( - href: href, - code: "1234", - continuationToken: "CT", - apiId: .telemetryApiIdV2SignInSubmitCode, - context: context - ) - - XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInSubmitCode) - } - - func test_submitAttributes_threadsApiId() throws { - let request = try sut.submitAttributes( - href: href, - attributes: ["city": "Redmond"], - continuationToken: "CT", - apiId: .telemetryApiIdV2SignUpSubmitAttributes, - context: context - ) - - XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignUpSubmitAttributes) - } - - func test_registerMethod_threadsApiId() throws { - let request = try sut.registerMethod( - href: href, - target: "email", - continuationToken: "CT", - apiId: .telemetryApiIdV2JITChallenge, - context: context - ) - - XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2JITChallenge) - } - func test_challenge_threadsApiId() throws { let request = try sut.challenge( href: href, @@ -162,12 +83,12 @@ final class MSALNativeAuthV2RequestProviderTests: XCTestCase { href: href, otp: "1234", continuationToken: "CT", - apiId: .telemetryApiIdV2MFASubmitChallenge, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, context: context ) XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2MFASubmitChallenge) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmitCode) } func test_updatePassword_usesPutAndThreadsApiId() throws { @@ -200,37 +121,37 @@ final class MSALNativeAuthV2RequestProviderTests: XCTestCase { func test_authorizeChallengeStart_usesAuthorizeChallengeEndpointAndThreadsApiId() throws { let request = try sut.authorizeChallengeStart( - apiId: .telemetryApiIdV2SignInWithPasswordStart, + apiId: .telemetryApiIdV2ResetPasswordStart, context: context ) XCTAssertEqual(request.urlRequest?.httpMethod, "POST") XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .authorizeChallenge)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInWithPasswordStart) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordStart) } func test_authorizeChallengeContinue_usesAuthorizeChallengeEndpointAndThreadsApiId() throws { let request = try sut.authorizeChallengeContinue( continuationToken: "CT", - apiId: .telemetryApiIdV2SignInWithCodeStart, + apiId: .telemetryApiIdV2ResetPasswordSubmit, context: context ) XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .authorizeChallenge)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInWithCodeStart) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmit) } func test_token_usesTokenEndpointAndKeepsRawJSONSerializer() throws { let request = try sut.token( code: "auth-code", scopes: ["scope1"], - apiId: .telemetryApiIdV2SignInSubmitCode, + apiId: .telemetryApiIdV2ResetPasswordSubmit, context: context ) XCTAssertEqual(request.urlRequest?.httpMethod, "POST") XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .token)) - XCTAssertEqual(apiId(of: request), .telemetryApiIdV2SignInSubmitCode) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmit) XCTAssertFalse(request.responseSerializer is MSALNativeAuthV2HALResponseSerializer) } } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index a19d6291da..3c0c8aef56 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -120,19 +120,6 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { XCTAssertEqual(result, .challengeRequired(continuationToken: "ct", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com")) } - func test_validateInteraction_challengeAction_multiFactor_returnsMFARequired() { - let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) - let response = makeResponse( - state: "interactionRequired", - action: "challenge", - continuationToken: "ct", - authenticationFactor: "multiFactor", - methods: [method] - ) - let result = sut.validateInteraction(context: context, .success(response)) - XCTAssertEqual(result, .mfaRequired(continuationToken: "ct", methods: [method], challengeHref: "https://contoso.com/challenge")) - } - func test_validateInteraction_verifyAction_returnsCodeRequired() { let response = makeResponse( state: "interactionRequired", @@ -190,12 +177,6 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - func test_validateInteraction_collectAttributesAction_withoutSubmitLink_failsWithMissingLink() { - let response = makeResponse(state: "interactionRequired", action: "collectAttributes", continuationToken: "ct") - let result = sut.validateInteraction(context: context, .success(response)) - XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) - } - func test_validateInteraction_continueState_returnsReadyToComplete() { let response = makeResponse(state: "continue", continuationToken: "ct") let result = sut.validateInteraction(context: context, .success(response)) From 2bfaf0bec0d71ba92f8ed0aacbb5bcdc314b6558 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 14:24:17 +0100 Subject: [PATCH 68/85] moved RetryExecutor to main to use in poll SSPR --- CLAUDE.md | 1 + MSAL/MSAL.xcodeproj/project.pbxproj | 12 +++---- .../v2/MSALNativeAuthFlowController.swift | 33 ++++++++----------- .../v2/MSALNativeAuthRetryExecutor.swift} | 8 ++--- .../MSALNativeAuthEmailCodeRetriever.swift | 3 +- 5 files changed, 25 insertions(+), 32 deletions(-) rename MSAL/{test/integration/native_auth/end_to_end/otp_code_retriever/RetryExecutor.swift => src/native_auth/controllers/v2/MSALNativeAuthRetryExecutor.swift} (92%) diff --git a/CLAUDE.md b/CLAUDE.md index 97b86f5e1b..55f9b9abe8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,6 +79,7 @@ Public headers MUST be in: 5. **Prefixes:** `MSAL` for public classes, `MSID` for IdentityCore internal 6. **Properties over ivars:** Use `@property` declarations 7. **Swift lint:** Native auth code must pass SwiftLint (line length: 150) +8. **Comments:** Only comment non-obvious rationale. Don't add comments that explain what the code already explains. **Example:** diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 3825580824..85aa60827d 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -88,7 +88,6 @@ 0529EA054FE400F5647FCBA5 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; 06AAD69B63B7A013959ECEF7 /* MSALNativeAuthFlowScenario.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */; }; 081C1B43CDAC5F4990EA68FB /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; - 0B808ECA169C3107F4335691 /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; 0D96DB3727850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */; }; 0D96DB3827850E8200DEAF87 /* MSALWipeCacheForAllAccountsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */; }; 0D96DB3A27850E8500DEAF87 /* MSALWipeCacheForAllAccountsConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D96DB3627850E3900DEAF87 /* MSALWipeCacheForAllAccountsConfig.m */; }; @@ -409,10 +408,10 @@ 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; - 31A0E8B0B69F886271896E3E /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; + BC9EAEE15D98DC1C6546DF0B /* MSALNativeAuthRetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* MSALNativeAuthRetryExecutor.swift */; }; 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; 3661378FB7B5DA5CCB37D76E /* MSALNativeAuthPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EA08303731BCACB308104F /* MSALNativeAuthPasswordRequiredState.swift */; }; 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */; }; @@ -508,6 +507,7 @@ 8DDF473F2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDF473E2A98FE1C00126A47 /* MSALNativeAuthRequiredAttribute.swift */; }; 8E0486CA55F25C1987E4067A /* MSALNativeAuthFlowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */; }; 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; + 1D2AE62369F492086D3C3924 /* MSALNativeAuthRetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* MSALNativeAuthRetryExecutor.swift */; }; 91AA24592BDF643A005037EA /* MSAL_Test_App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA24582BDF643A005037EA /* MSAL_Test_App.swift */; }; 91AA245B2BDF643A005037EA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91AA245A2BDF643A005037EA /* ContentView.swift */; }; 91AA245D2BDF6440005037EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 91AA245C2BDF6440005037EA /* Assets.xcassets */; }; @@ -2341,7 +2341,7 @@ 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseErrorHandler.swift; sourceTree = ""; }; 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenRequestHandling.swift; sourceTree = ""; }; 475F1413DA1D76D5EF31F4EC /* MailTMHTTPClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailTMHTTPClient.swift; sourceTree = ""; }; - 49AAD919E560052DA700D2DA /* RetryExecutor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RetryExecutor.swift; sourceTree = ""; }; + 49AAD919E560052DA700D2DA /* MSALNativeAuthRetryExecutor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthRetryExecutor.swift; sourceTree = ""; }; 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthRegistrationRequiredState.swift; sourceTree = ""; }; 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerResponse.swift; sourceTree = ""; }; @@ -3252,6 +3252,7 @@ isa = PBXGroup; children = ( 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */, + 49AAD919E560052DA700D2DA /* MSALNativeAuthRetryExecutor.swift */, B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */, 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */, 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */, @@ -3412,7 +3413,6 @@ 28A277D82C22ED5E00D95E00 /* MSALNativeAuthEmailCodeRetriever.swift */, C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */, 475F1413DA1D76D5EF31F4EC /* MailTMHTTPClient.swift */, - 49AAD919E560052DA700D2DA /* RetryExecutor.swift */, ); path = otp_code_retriever; sourceTree = ""; @@ -7062,7 +7062,6 @@ DED1F0A12DD64544009CB97A /* MSALNativeAuthSignInJITEndToEndTests.swift in Sources */, AE64B3751432B2A8DD6C7FAB /* MailTMConstants.swift in Sources */, 64463489E8DC5172D49F98FF /* MailTMHTTPClient.swift in Sources */, - 31A0E8B0B69F886271896E3E /* RetryExecutor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7510,6 +7509,7 @@ 28DE70D629FAC16700EB75AA /* MSALNativeAuthSignInResponseValidator.swift in Sources */, 965F0298A9F7FF81A447DEBB /* MSALNativeAuthFlowInternalState.swift in Sources */, 91656BB678CD68B8F2F92DD7 /* MSALNativeAuthFlowResult.swift in Sources */, + 1D2AE62369F492086D3C3924 /* MSALNativeAuthRetryExecutor.swift in Sources */, C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */, 368B857871B6FB27BCB2C924 /* MSALNativeAuthFlowControllerResponse.swift in Sources */, CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */, @@ -7844,6 +7844,7 @@ DE8DC4512C66218900534E8F /* MSALNativeAuthInternalError.swift in Sources */, 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */, 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */, + BC9EAEE15D98DC1C6546DF0B /* MSALNativeAuthRetryExecutor.swift in Sources */, 5F6B9DF59F4E65251CB02F6D /* MSALNativeAuthFlowContinuationState.swift in Sources */, 0529EA054FE400F5647FCBA5 /* MSALNativeAuthFlowControllerResponse.swift in Sources */, D5449AE1C2AE8608DA837967 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */, @@ -8331,7 +8332,6 @@ DED1F0A02DD64544009CB97A /* MSALNativeAuthSignInJITEndToEndTests.swift in Sources */, 022239DBCF2EF4AD83359DD3 /* MailTMConstants.swift in Sources */, 9313B1799984552C778C5E5C /* MailTMHTTPClient.swift in Sources */, - 0B808ECA169C3107F4335691 /* RetryExecutor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index d700cb11fb..bfc4453928 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -36,7 +36,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private let kNumberOfTimesToRetryPollCompletionCall = 5 // TODO: Confirm this is needed and server doesn't send - private let pollIntervalNanoseconds: UInt64 = 1_500_000_000 // 1.5s + private let pollIntervalSeconds: Double = 1.5 // delay between poll attempts init( config: MSALNativeAuthInternalConfiguration, @@ -192,12 +192,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati return interactionFailure(updateResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) } - var completionToken: String? - for attempt in 0.. 0 { - try? await Task.sleep(nanoseconds: pollIntervalNanoseconds) - } - + let retryExecutor = MSALNativeAuthRetryExecutor(delays: [pollIntervalSeconds]) + let terminalPollResult = await retryExecutor.execute( + maxAttempts: kNumberOfTimesToRetryPollCompletionCall + ) { () -> MSALNativeAuthV2InteractionValidatedResponse? in let pollResult = await performInteraction(context: context) { try self.requestProvider.poll( href: pollHref, @@ -207,24 +205,15 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } - switch pollResult { - case .readyToComplete(let token): - completionToken = token - case .pollInProgress(let token, _): + if case .pollInProgress(let token, _) = pollResult { pollToken = token - continue - case .error: - return interactionFailure(pollResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) - default: - return interactionFailure(pollResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) + return nil } - if completionToken != nil { - break - } + return pollResult } - guard let completionToken = completionToken else { + guard let terminalPollResult = terminalPollResult else { return failure( .error( MSALNativeAuthFlowError( @@ -237,6 +226,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } + guard case .readyToComplete(let completionToken) = terminalPollResult else { + return interactionFailure(terminalPollResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) + } + return await completeWithToken( flowScenario: continuation.flowScenario, continuationToken: completionToken, diff --git a/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/RetryExecutor.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthRetryExecutor.swift similarity index 92% rename from MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/RetryExecutor.swift rename to MSAL/src/native_auth/controllers/v2/MSALNativeAuthRetryExecutor.swift index 2dca5fe8c4..60a7a14148 100644 --- a/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/RetryExecutor.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthRetryExecutor.swift @@ -20,15 +20,13 @@ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - +// THE SOFTWARE. import Foundation /// Runs an async operation with progressive delays between attempts until it yields a non-nil -/// result or the attempts are exhausted. Extracted (per PR #3040 review feedback) so the -/// polling/backoff schedule is a single reusable component. -struct RetryExecutor { +/// result or the attempts are exhausted. +struct MSALNativeAuthRetryExecutor { /// Delays (seconds) applied between attempts. The last value is reused if attempts exceed its count. let delays: [Double] diff --git a/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift b/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift index b7a619e403..69ce53d87c 100644 --- a/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift +++ b/MSAL/test/integration/native_auth/end_to_end/otp_code_retriever/MSALNativeAuthEmailCodeRetriever.swift @@ -23,6 +23,7 @@ // THE SOFTWARE. import Foundation +@testable import MSAL /// Retrieves email OTP codes from the mail.tm disposable-email service (https://docs.mail.tm). /// @@ -182,7 +183,7 @@ class MSALNativeAuthEmailCodeRetriever { print("Call connectToExistingAccount()/login() before reading messages") return nil } - let executor = RetryExecutor(delays: MailTMConstants.progressiveDelays) + let executor = MSALNativeAuthRetryExecutor(delays: MailTMConstants.progressiveDelays) let code = await executor.execute(maxAttempts: maxRetries) { await self.attemptReadOtpCode() } From 0a2b0a4d329e9ba386f64878be30cde38e214cdd Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 14:32:46 +0100 Subject: [PATCH 69/85] setup MSALNativeAuthFlowScenario as case iterable --- .../native_auth/controllers/v2/MSALNativeAuthFlowResult.swift | 2 +- .../public/state_machine/v2/MSALNativeAuthFlowScenario.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift index c56350eee8..ff0969b219 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift @@ -28,7 +28,7 @@ import Foundation extension MSALNativeAuthFlowScenario { /// The server-driven flows the SDK follows when resolving `authorize-challenge` links. - static let authorizeChallengeFlows: [MSALNativeAuthFlowScenario] = [.signUp, .signIn, .passwordReset, .unknown] + static let authorizeChallengeFlows = MSALNativeAuthFlowScenario.allCases /// The `authorize-challenge` link relation this flow follows. var link: String { diff --git a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift index b292eb85e4..9474f6497b 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowScenario.swift @@ -33,7 +33,7 @@ import Foundation /// /// - Warning: This API is experimental. It may be changed in the future without notice. Do not use in production applications. @objc -public enum MSALNativeAuthFlowScenario: Int { +public enum MSALNativeAuthFlowScenario: Int, CaseIterable { /// The scenario could not be determined. This is the default value and should not normally be /// reported to the app; it acts as a safe placeholder until a concrete flow scenario is resolved. From 29f5e8da606596afadef094393f70a58aef9c307 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 14:37:44 +0100 Subject: [PATCH 70/85] remove not used cache accessor --- .../native_auth/controllers/MSALNativeAuthTokenController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift index cda9aabdfa..a1e75aeb87 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift @@ -33,7 +33,6 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController, MSALNativeAut let factory: MSALNativeAuthResultBuildable private let requestProvider: MSALNativeAuthTokenRequestProviding private let responseValidator: MSALNativeAuthTokenResponseValidating - private let cacheAccessor: MSALNativeAuthCacheInterface private let tokenCacher: MSALNativeAuthTokenCacher init( @@ -46,7 +45,6 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController, MSALNativeAut self.requestProvider = requestProvider self.factory = factory self.responseValidator = responseValidator - self.cacheAccessor = cacheAccessor self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) super.init( clientId: clientId From b400f53ae0ec0ebe3140b0f6471adfab53901bc1 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 14:39:26 +0100 Subject: [PATCH 71/85] Removed deprecated messages --- .../public/MSALNativeAuthPublicClientApplication.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index d7adae16e9..e48d2fd142 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -152,7 +152,6 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic /// - Parameters: /// - parameters: Parameters used for the Sign Up flow. /// - delegate: Delegate that receives callbacks for the Sign Up flow. - @available(*, deprecated, message: "Use signUpV2(parameters:delegate:) instead") public func signUp( parameters: MSALNativeAuthSignUpParameters, delegate: SignUpStartDelegate @@ -188,7 +187,6 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic /// - Parameters: /// - parameters: Parameters used for the Sign In flow. /// - delegate: Delegate that receives callbacks for the Sign In flow. - @available(*, deprecated, message: "Use signInV2(parameters:delegate:) instead") public func signIn( parameters: MSALNativeAuthSignInParameters, delegate: SignInStartDelegate @@ -236,7 +234,6 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic /// - Parameters: /// - parameters: Parameters used for the Reset Password flow. /// - delegate: Delegate that receives callbacks for the Reset Password flow. - @available(*, deprecated, message: "Use resetPasswordV2(parameters:delegate:) instead") public func resetPassword( parameters: MSALNativeAuthResetPasswordParameters, delegate: ResetPasswordStartDelegate From 8e7741907a3cbd980975d985b98378d93303474a Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 16:24:54 +0100 Subject: [PATCH 72/85] made mapInteraction testable and added tests --- .../v2/MSALNativeAuthFlowController.swift | 2 +- .../MSALNativeAuthFlowControllerTests.swift | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index bfc4453928..96c42a8d4e 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -341,7 +341,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // Maps a validated interaction response onto a controller response, building the next // required state or, on a terminal response, running the authorize-challenge → token completion. - private func mapInteraction( + func mapInteraction( _ result: MSALNativeAuthV2InteractionValidatedResponse, flowScenario: MSALNativeAuthFlowScenario, username: String?, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 64f711d934..e8985aed96 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -34,6 +34,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { private var validatorMock: MSALNativeAuthV2ResponseValidatorMock! private var cacheAccessorMock: MSALNativeAuthCacheAccessorMock! private var resultFactoryMock: MSALNativeAuthResultFactoryMock! + private let context = MSALNativeAuthRequestContext(correlationId: UUID()) override func setUpWithError() throws { try super.setUpWithError() @@ -239,4 +240,79 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { } XCTAssertTrue(requestProviderMock.challengeCalled) } + + // MARK: - mapInteraction (branch logic) + + func test_mapInteraction_codeRequired_usesServerSentTo_whenPresent() async { + let state = await mapCodeRequired(sentTo: "u***@contoso.com", fallbackHint: "fallback@contoso.com") + XCTAssertEqual(state?.sentTo, "u***@contoso.com") + } + + func test_mapInteraction_codeRequired_fallsBackToHint_whenServerSentToEmpty() async { + let state = await mapCodeRequired(sentTo: "", fallbackHint: "fallback@contoso.com") + XCTAssertEqual(state?.sentTo, "fallback@contoso.com") + } + + func test_mapInteraction_codeRequired_emptyServerSentToAndNoHint_yieldsEmptyDisplay() async { + let state = await mapCodeRequired(sentTo: "", fallbackHint: nil) + XCTAssertEqual(state?.sentTo, "") + } + + func test_mapInteraction_error_invalidCode_isRecoverable() async { + let newState = await mapErrorNewState(type: .invalidCode) + XCTAssertNotNil(newState) + } + + func test_mapInteraction_error_invalidPassword_isRecoverable() async { + let newState = await mapErrorNewState(type: .invalidPassword) + XCTAssertNotNil(newState) + } + + func test_mapInteraction_error_generalError_isNotRecoverable() async { + let newState = await mapErrorNewState(type: .generalError) + XCTAssertNil(newState) + } + + private func mapCodeRequired(sentTo: String, fallbackHint: String?) async -> MSALNativeAuthCodeRequiredState? { + let response = await sut.mapInteraction( + .codeRequired( + continuationToken: "ct", + verifyHref: "https://contoso.com/verify", + resendHref: "https://contoso.com/resend", + sentTo: sentTo, + channelType: MSALNativeAuthChannelType(value: "email"), + codeLength: 8 + ), + flowScenario: .passwordReset, + username: "user@contoso.com", + scopes: [], + apiId: .telemetryApiIdResetPassword, + event: nil, + context: context, + fallbackHint: fallbackHint + ) + guard case .actionRequired(let state) = response.result else { + return nil + } + return state as? MSALNativeAuthCodeRequiredState + } + + private func mapErrorNewState(type: MSALNativeAuthFlowError.ErrorType) async -> MSALNativeAuthFlowInternalState? { + let recoverableState = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) + let response = await sut.mapInteraction( + .error(MSALNativeAuthFlowError(type: type)), + flowScenario: .passwordReset, + username: "user@contoso.com", + scopes: [], + apiId: .telemetryApiIdResetPassword, + event: nil, + context: context, + recoverableState: recoverableState + ) + guard case .error(_, let newState) = response.result else { + return nil + } + return newState + } + } From 86394ce77b044dae21d82918e7365e90e4be8d4b Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 16:41:51 +0100 Subject: [PATCH 73/85] Add redirect back --- .../v2/MSALNativeAuthFlowController.swift | 3 +++ .../MSALNativeAuthFlowResponseDispatcher.swift | 3 +-- .../v2/MSALNativeAuthFlowResult.swift | 2 +- .../v2/MSALNativeAuthHALResponse.swift | 7 +++++++ .../MSALNativeAuthV2ResponseValidator.swift | 4 ++++ .../MSALNativeAuthV2ValidatedResponses.swift | 4 ++++ .../v2/MSALNativeAuthFlowControllerTests.swift | 15 +++++++++++++++ ...LNativeAuthFlowResponseDispatcherTests.swift | 2 +- ...MSALNativeAuthV2ResponseValidatorTests.swift | 17 +++++++++++++++++ 9 files changed, 53 insertions(+), 4 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 96c42a8d4e..cb3c364db7 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -395,6 +395,9 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati .error(error: error, newState: error.isInvalidCode || error.type == .invalidPassword ? recoverableState : nil), context: context, scenario: flowScenario ) + case .browserRequired: + stopTelemetryEvent(event, context: context) + return response(.browserRequired, context: context, scenario: flowScenario) default: return interactionFailure(result, event: event, context: context, scenario: flowScenario, newState: nil) } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift index 3c9c105d26..334edf220e 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.swift @@ -43,10 +43,9 @@ struct MSALNativeAuthFlowResponseDispatcher { response.telemetryUpdate?(.success(())) case .error(let error, _): await delegate.onFlowError(error: error, scenario: scenario) - case .browserRequired(let url, _): + case .browserRequired: let error = MSALNativeAuthFlowError( type: .browserRequired, - errorDescription: "The flow must continue in a web browser: \(url.absoluteString)", correlationId: response.correlationId ) await delegate.onFlowError(error: error, scenario: scenario) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift index ff0969b219..329e6e62a4 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResult.swift @@ -50,5 +50,5 @@ enum MSALNativeAuthFlowResult { case actionRequired(state: MSALNativeAuthState) case completed(MSALNativeAuthUserAccountResult) case error(error: MSALNativeAuthFlowError, newState: MSALNativeAuthFlowInternalState?) - case browserRequired(url: URL, newState: MSALNativeAuthFlowInternalState) + case browserRequired } diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 608516ea28..1500b89b1c 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -101,6 +101,13 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { return state == "continue" } + /// `error == redirect_to_web` / `state == webFallbackRequired`: the server cannot continue this + /// flow natively and the app must fall back to interactive `acquireToken`. The `webFallback` link + /// is not used by the SDK. + var isWebFallbackRequired: Bool { + return error?.code == "redirect_to_web" || state == "webFallbackRequired" + } + func href(forRelation relation: String) -> String? { return links[relation] } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index a13b44ef4e..ce6ce9ed57 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -83,6 +83,10 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .failure(let error): return .error(flowError(from: error, context: context)) case .success(let response): + if response.isWebFallbackRequired { + MSALNativeAuthLogger.log(level: .info, context: context, format: "interaction: web fallback required") + return .browserRequired + } if let error = response.error { return .error(flowError(from: error, context: context)) } diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift index 0efe40a62b..a903324ee5 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift @@ -62,6 +62,8 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { case pollInProgress(continuationToken: String, pollHref: String) /// `state == continue`: the flow is ready to complete (call `authorize-challenge`). case readyToComplete(continuationToken: String) + /// `error == redirect_to_web` / `state == webFallbackRequired`: the flow must continue in a browser. + case browserRequired case error(MSALNativeAuthFlowError) static func == (lhs: Self, rhs: Self) -> Bool { @@ -76,6 +78,8 @@ enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { return lToken == rToken && lHref == rHref case let (.readyToComplete(lToken), .readyToComplete(rToken)): return lToken == rToken + case (.browserRequired, .browserRequired): + return true case let (.error(lError), .error(rError)): return lError.type == rError.type default: diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index e8985aed96..a260df3813 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -273,6 +273,21 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { XCTAssertNil(newState) } + func test_mapInteraction_browserRequired_returnsBrowserRequiredResult() async { + let response = await sut.mapInteraction( + .browserRequired, + flowScenario: .passwordReset, + username: "user@contoso.com", + scopes: [], + apiId: .telemetryApiIdResetPassword, + event: nil, + context: context() + ) + guard case .browserRequired = response.result else { + return XCTFail("Expected browserRequired, got \(response.result)") + } + } + private func mapCodeRequired(sentTo: String, fallbackHint: String?) async -> MSALNativeAuthCodeRequiredState? { let response = await sut.mapInteraction( .codeRequired( diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift index 3e8e5dbbae..cd4c133cf4 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.swift @@ -75,7 +75,7 @@ final class MSALNativeAuthFlowResponseDispatcherTests: XCTestCase { let delegate = BaseDelegateSpy() var telemetryResult: Result? let response = MSALNativeAuthFlowControllerResponse( - .browserRequired(url: URL(string: "https://contoso.com/fallback")!, newState: makeInternalState()), + .browserRequired, correlationId: UUID(), scenario: .signUp, telemetryUpdate: { telemetryResult = $0 } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift index 3c0c8aef56..e51da2364e 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift @@ -183,6 +183,23 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { XCTAssertEqual(result, .readyToComplete(continuationToken: "ct")) } + func test_validateInteraction_webFallbackRequiredState_returnsBrowserRequired() { + let response = makeResponse( + state: "webFallbackRequired", + continuationToken: "ct", + links: ["webFallback": "https://contoso.com/oauth2/v2.0/authorize"] + ) + let result = sut.validateInteraction(context: context, .success(response)) + XCTAssertEqual(result, .browserRequired) + } + + func test_validateInteraction_redirectToWebError_returnsBrowserRequired() { + let serverError = MSALNativeAuthHALResponse.ServerError(code: "redirect_to_web", message: nil, innerErrorCode: nil, correlationId: nil) + let response = makeResponse(continuationToken: "ct", error: serverError) + let result = sut.validateInteraction(context: context, .success(response)) + XCTAssertEqual(result, .browserRequired) + } + func test_validateInteraction_userNotFound_mapsToUserNotFound() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "AADSTS50034 user not found", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) From 2237fe65de34ab72469c4951b3c475b6d0a273eb Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 16:50:51 +0100 Subject: [PATCH 74/85] Added clarification comment --- .../v2/validator/MSALNativeAuthV2ResponseValidator.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift index ce6ce9ed57..00353570ba 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift @@ -85,6 +85,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin case .success(let response): if response.isWebFallbackRequired { MSALNativeAuthLogger.log(level: .info, context: context, format: "interaction: web fallback required") + // The URL is not returned here as the developer needs to invoke Auth UX return .browserRequired } if let error = response.error { From c83c369fabda7892185706f1e9b3747867650390 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Mon, 27 Jul 2026 17:03:54 +0100 Subject: [PATCH 75/85] Rename V2 response validator to parser The V2 'validator' returns a response model rather than a valid/invalid result, so 'parser' better reflects its behavior. Renames the protocol, class, validated-response enums, methods, files and enclosing group from validator/Validated to parser/Parsed. V1 validators and account validation are unaffected. Also fixes a latent context: context() call in the browserRequired mapInteraction test that had never been compiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecf13a9d-6bea-4c2f-834e-ef32ed967500 --- MSAL/MSAL.xcodeproj/project.pbxproj | 56 +++++------ .../v2/MSALNativeAuthFlowController.swift | 34 +++---- .../MSALNativeAuthV2ParsedResponses.swift} | 10 +- .../MSALNativeAuthV2ResponseParser.swift} | 26 ++--- .../MSALNativeAuthFlowControllerTests.swift | 28 +++--- ... MSALNativeAuthV2ResponseParserMock.swift} | 30 +++--- ...MSALNativeAuthV2ResponseParserTests.swift} | 94 +++++++++---------- 7 files changed, 139 insertions(+), 139 deletions(-) rename MSAL/src/native_auth/network/responses/v2/{validator/MSALNativeAuthV2ValidatedResponses.swift => parser/MSALNativeAuthV2ParsedResponses.swift} (93%) rename MSAL/src/native_auth/network/responses/v2/{validator/MSALNativeAuthV2ResponseValidator.swift => parser/MSALNativeAuthV2ResponseParser.swift} (94%) rename MSAL/test/unit/native_auth/mock/v2/{MSALNativeAuthV2ResponseValidatorMock.swift => MSALNativeAuthV2ResponseParserMock.swift} (65%) rename MSAL/test/unit/native_auth/network/v2/{MSALNativeAuthV2ResponseValidatorTests.swift => MSALNativeAuthV2ResponseParserTests.swift} (70%) diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 85aa60827d..b8f6a5f883 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -405,7 +405,7 @@ 28FDC4AE2A38D81100E38BE1 /* MSALNativeAuthSignInControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28FDC4AB2A38D7D200E38BE1 /* MSALNativeAuthSignInControllerMock.swift */; }; 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; - 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; + 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseParser.swift */; }; 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; @@ -422,7 +422,7 @@ 3F2E65884A64B912E42B512D /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; 42E1FE910A9592561A2F44DC /* MSALNativeAuthStrongAuthVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEA06244B0DACD04D94193D /* MSALNativeAuthStrongAuthVerificationRequiredState.swift */; }; 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB6C9AE5AF1A99BF232126 /* MSALNativeAuthV2TokenParameters.swift */; }; - 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ParsedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ParsedResponses.swift */; }; 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */; }; 4CEDE2C62AFBCC69A07C8652 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; 4F6C95BC33A85725CB3F2185 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; @@ -465,7 +465,7 @@ 7207E6402FA97BBD008F6803 /* MSALDeviceTokenParametersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7207E63E2FA97BBC008F6803 /* MSALDeviceTokenParametersTests.m */; }; 7207E6432FA97BE4008F6803 /* MSALDeviceTokenResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7207E6422FA97BE3008F6803 /* MSALDeviceTokenResultTests.m */; }; 7207E6442FA97BE4008F6803 /* MSALDeviceTokenResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7207E6422FA97BE3008F6803 /* MSALDeviceTokenResultTests.m */; }; - 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */; }; + 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseParser.swift */; }; 7233F07F2F885A4A009C9602 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7233F0812F885A4A009C9602 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7233F0822F885A4A009C9602 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -638,8 +638,8 @@ A09AAFC424C00B3700C324DE /* MSALAuthenticationSchemeProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E3658A6247F2BB60044A072 /* MSALAuthenticationSchemeProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A370E8BCE6A05E05ECC63027 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; A4B46554DA558BD5457129CF /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift */; }; - A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; - A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */; }; + A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */; }; + A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */; }; A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; AA5AB06A9DD86202FD19BFC8 /* MSALNativeAuthV2RequestTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */; }; AE64B3751432B2A8DD6C7FAB /* MailTMConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */; }; @@ -1043,7 +1043,7 @@ B2FBB3DB28F72A5700A3591C /* MSALWPJMetaData+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FBB3D228F72A5700A3591C /* MSALWPJMetaData+Internal.h */; }; B2FE601B20E5BB5800502BA6 /* MSAL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D65A6F431E3FD30A00C69FBA /* MSAL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B311DA009515BDDE5FDF3679 /* MSALNativeAuthMFARequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FE5D3DE054DBA691A401E3D /* MSALNativeAuthMFARequiredState.swift */; }; - B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */; }; + B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ParsedResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ParsedResponses.swift */; }; B3E12C5ECC553A95521CFEFA /* MSALNativeAuthNewPasswordRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8273A2EAA82AE303691B976F /* MSALNativeAuthNewPasswordRequiredState.swift */; }; B4CDF4FB20138CF27310258B /* MSALNativeAuthV2RequestProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */; }; B5A1E2121EE36D3BC037113D /* MSALNativeAuthV2Requestable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A191DDE2C877A82F4C55528 /* MSALNativeAuthV2Requestable.swift */; }; @@ -1058,7 +1058,7 @@ CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; - D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; + D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */; }; D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */; }; D3C4A02BF6F6E02B8D58ACE8 /* MSALNativeAuthV2RequestProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */; }; @@ -1786,7 +1786,7 @@ F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; F819D42E8772D0CDAB08945A /* MSALNativeAuthFlowResponseDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */; }; - F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */; }; + F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */; }; FADE0000000000000000AA02 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; FADE0000000000000000AA03 /* HALResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADE0000000000000000AA01 /* HALResource.swift */; }; FE0A0B00000000000000A001 /* MSALNativeAuthResetPasswordParametersV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0A0B00000000000000F001 /* MSALNativeAuthResetPasswordParametersV2.swift */; }; @@ -2154,7 +2154,7 @@ 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorityURLFormat.swift; sourceTree = ""; }; 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolverTests.swift; sourceTree = ""; }; 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBody.swift; sourceTree = ""; }; - 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ValidatedResponses.swift; sourceTree = ""; }; + 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ParsedResponses.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ParsedResponses.swift; sourceTree = ""; }; 1E04571F24BD5A7D00444756 /* MSALCacheItemDetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCacheItemDetailViewController.h; sourceTree = ""; }; 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCacheItemDetailViewController.m; sourceTree = ""; }; 1E1A2E052256D194001009ED /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -2359,7 +2359,7 @@ 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthMFAVerificationRequiredState.swift; sourceTree = ""; }; 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerMock.swift; sourceTree = ""; }; 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALAction.swift; sourceTree = ""; }; - 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidator.swift; sourceTree = ""; }; + 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseParser.swift; sourceTree = ""; }; 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALDeviceTokenResult+Internal.h"; sourceTree = ""; }; 7207E63E2FA97BBC008F6803 /* MSALDeviceTokenParametersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenParametersTests.m; sourceTree = ""; }; 7207E6422FA97BE3008F6803 /* MSALDeviceTokenResultTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDeviceTokenResultTests.m; sourceTree = ""; }; @@ -2476,7 +2476,7 @@ 9BE7E3D42A1CF51500CC3A62 /* MSALNativeAuthResetPasswordValidatedResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthResetPasswordValidatedResponses.swift; sourceTree = ""; }; 9D292B0F28F05696007FE93C /* MSALWPJMetaData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSALWPJMetaData.m; sourceTree = ""; }; 9DA6473528EC2FF10014F44F /* MSALWPJMetaData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALWPJMetaData.h; sourceTree = ""; }; - 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorMock.swift; sourceTree = ""; }; + 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseParserMock.swift; sourceTree = ""; }; A0274CBD24B432B100BD198D /* MSALAuthSchemeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALAuthSchemeTests.m; sourceTree = ""; }; A0274CD724B54A4E00BD198D /* MSALDevicePopManagerUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALDevicePopManagerUtil.m; sourceTree = ""; }; A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; @@ -2646,7 +2646,7 @@ BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesInvalidState.swift; sourceTree = ""; }; C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HALResponseSerializer.swift; sourceTree = ""; }; C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowController.swift; sourceTree = ""; }; - C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseValidatorTests.swift; sourceTree = ""; }; + C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ResponseParserTests.swift; sourceTree = ""; }; C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ParametersTests.swift; sourceTree = ""; }; C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MailTMConstants.swift; sourceTree = ""; }; CF017CDD211895E02588AA7E /* MSALNativeAuthV2RequestTarget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestTarget.swift; sourceTree = ""; }; @@ -3264,14 +3264,14 @@ path = v2; sourceTree = ""; }; - 1A153D161101EAF09A32E906 /* validator */ = { + 1A153D161101EAF09A32E906 /* parser */ = { isa = PBXGroup; children = ( - 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ValidatedResponses.swift */, - 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseValidator.swift */, + 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ParsedResponses.swift */, + 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseParser.swift */, ); - name = validator; - path = validator; + name = parser; + path = parser; sourceTree = ""; }; 2811CDCF296F16DE007BA21B /* controllers */ = { @@ -3678,7 +3678,7 @@ FADE0000000000000000AA01 /* HALResource.swift */, C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */, 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */, - 1A153D161101EAF09A32E906 /* validator */, + 1A153D161101EAF09A32E906 /* parser */, ); name = v2; path = v2; @@ -3687,7 +3687,7 @@ 4E0AA7DA6DFC949218EEA54E /* v2 */ = { isa = PBXGroup; children = ( - C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseValidatorTests.swift */, + C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */, 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */, C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */, 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */, @@ -3767,7 +3767,7 @@ children = ( 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */, 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */, - 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseValidatorMock.swift */, + 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */, ); name = v2; path = v2; @@ -7520,8 +7520,8 @@ FADE0000000000000000AA02 /* HALResource.swift in Sources */, 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, - B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, - 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseValidator.swift in Sources */, + B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ParsedResponses.swift in Sources */, + 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseParser.swift in Sources */, 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */, F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */, D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, @@ -7855,8 +7855,8 @@ FADE0000000000000000AA03 /* HALResource.swift in Sources */, 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, - 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ValidatedResponses.swift in Sources */, - 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseValidator.swift in Sources */, + 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ParsedResponses.swift in Sources */, + 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseParser.swift in Sources */, 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */, 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */, 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, @@ -8066,9 +8066,9 @@ E22427EA2B065EAE0006C55E /* SignUpVerifyCodeDelegateDispatcherTests.swift in Sources */, A370E8BCE6A05E05ECC63027 /* MSALNativeAuthFlowControllerMock.swift in Sources */, 5E471E84AA33CFA840BBA964 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, - D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, + D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */, E04298BA8ED8FBE431F561A2 /* MSALNativeAuthFlowControllerTests.swift in Sources */, - A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, + A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseParserTests.swift in Sources */, 192F74D7E3825C5CDCF50CEB /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */, 547D9B6A1EA16110560F531F /* MSALNativeAuthV2ParametersTests.swift in Sources */, B4CDF4FB20138CF27310258B /* MSALNativeAuthV2RequestProviderTests.swift in Sources */, @@ -8256,9 +8256,9 @@ DE8DC56C2C66221C00534E8F /* MSALNativeLoggingTests.swift in Sources */, A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */, 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, - F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseValidatorMock.swift in Sources */, + F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */, 9D57981C31A9157AF52A29B9 /* MSALNativeAuthFlowControllerTests.swift in Sources */, - A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseValidatorTests.swift in Sources */, + A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseParserTests.swift in Sources */, 827CE360F94F0A5BCA875193 /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */, C34EB4B71143A1078F0B72E2 /* MSALNativeAuthV2ParametersTests.swift in Sources */, D3C4A02BF6F6E02B8D58ACE8 /* MSALNativeAuthV2RequestProviderTests.swift in Sources */, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index cb3c364db7..9f6317d971 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -30,7 +30,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private let config: MSALNativeAuthInternalConfiguration private let requestProvider: MSALNativeAuthV2RequestProviding - private let responseValidator: MSALNativeAuthV2ResponseValidating + private let responseParser: MSALNativeAuthV2ResponseParsing private let resultFactory: MSALNativeAuthResultBuildable private let tokenCacher: MSALNativeAuthTokenCacher @@ -41,13 +41,13 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati init( config: MSALNativeAuthInternalConfiguration, requestProvider: MSALNativeAuthV2RequestProviding, - responseValidator: MSALNativeAuthV2ResponseValidating, + responseParser: MSALNativeAuthV2ResponseParsing, cacheAccessor: MSALNativeAuthCacheInterface, resultFactory: MSALNativeAuthResultBuildable ) { self.config = config self.requestProvider = requestProvider - self.responseValidator = responseValidator + self.responseParser = responseParser self.resultFactory = resultFactory self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) super.init(clientId: config.clientId) @@ -57,7 +57,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati self.init( config: config, requestProvider: MSALNativeAuthV2RequestProvider(config: config), - responseValidator: MSALNativeAuthV2ResponseValidator(), + responseParser: MSALNativeAuthV2ResponseParser(), cacheAccessor: cacheAccessor, resultFactory: MSALNativeAuthResultFactory(config: config, cacheAccessor: cacheAccessor) ) @@ -195,7 +195,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati let retryExecutor = MSALNativeAuthRetryExecutor(delays: [pollIntervalSeconds]) let terminalPollResult = await retryExecutor.execute( maxAttempts: kNumberOfTimesToRetryPollCompletionCall - ) { () -> MSALNativeAuthV2InteractionValidatedResponse? in + ) { () -> MSALNativeAuthV2InteractionParsedResponse? in let pollResult = await performInteraction(context: context) { try self.requestProvider.poll( href: pollHref, @@ -297,11 +297,11 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati flowScenario: MSALNativeAuthFlowScenario, apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext - ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + ) async -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { let result: Result = await send { try self.requestProvider.authorizeChallengeStart(apiId: apiId, context: context) } - return responseValidator.validateAuthorizeChallenge(context: context, result, flowScenario: flowScenario) + return responseParser.parseAuthorizeChallenge(context: context, result, flowScenario: flowScenario) } private func performAuthorizeChallengeContinue( @@ -309,19 +309,19 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati continuationToken: String, apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext - ) async -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + ) async -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { let result: Result = await send { try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, apiId: apiId, context: context) } - return responseValidator.validateAuthorizeChallenge(context: context, result, flowScenario: flowScenario) + return responseParser.parseAuthorizeChallenge(context: context, result, flowScenario: flowScenario) } private func performInteraction( context: MSALNativeAuthRequestContext, requestBuilder: @escaping () throws -> MSIDHttpRequest - ) async -> MSALNativeAuthV2InteractionValidatedResponse { + ) async -> MSALNativeAuthV2InteractionParsedResponse { let result: Result = await send(requestBuilder) - return responseValidator.validateInteraction(context: context, result) + return responseParser.parseInteraction(context: context, result) } private func send( @@ -339,10 +339,10 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MARK: - Result mapping - // Maps a validated interaction response onto a controller response, building the next + // Maps a parsed interaction response onto a controller response, building the next // required state or, on a terminal response, running the authorize-challenge → token completion. func mapInteraction( - _ result: MSALNativeAuthV2InteractionValidatedResponse, + _ result: MSALNativeAuthV2InteractionParsedResponse, flowScenario: MSALNativeAuthFlowScenario, username: String?, scopes: [String], @@ -573,13 +573,13 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } private func failure( - _ validated: MSALNativeAuthV2AuthorizeChallengeValidatedResponse, + _ parsed: MSALNativeAuthV2AuthorizeChallengeParsedResponse, event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, scenario: MSALNativeAuthFlowScenario ) -> MSALNativeAuthFlowControllerResponse { let error: MSALNativeAuthFlowError - if case .error(let flowError) = validated { + if case .error(let flowError) = parsed { error = flowError } else { error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected authorize-challenge response") @@ -589,14 +589,14 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } private func interactionFailure( - _ validated: MSALNativeAuthV2InteractionValidatedResponse, + _ parsed: MSALNativeAuthV2InteractionParsedResponse, event: MSIDTelemetryAPIEvent?, context: MSALNativeAuthRequestContext, scenario: MSALNativeAuthFlowScenario, newState: MSALNativeAuthFlowInternalState? ) -> MSALNativeAuthFlowControllerResponse { let error: MSALNativeAuthFlowError - if case .error(let flowError) = validated { + if case .error(let flowError) = parsed { error = flowError } else { error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected server response") diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ParsedResponses.swift similarity index 93% rename from MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift rename to MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ParsedResponses.swift index a903324ee5..b19901dafb 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ValidatedResponses.swift +++ b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ParsedResponses.swift @@ -24,8 +24,8 @@ import Foundation -/// Validated outcome of an `authorize-challenge` call. -enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { +/// Parsed outcome of an `authorize-challenge` call. +enum MSALNativeAuthV2AuthorizeChallengeParsedResponse: Equatable { /// `401` carrying the continuation token and the resolved entry link for the flow /// (`sign_up` / `sign_in` / `reset_password`). case continuationToken(continuationToken: String, href: String) @@ -47,11 +47,11 @@ enum MSALNativeAuthV2AuthorizeChallengeValidatedResponse: Equatable { } } -/// Validated outcome of an SSPR interaction step (resetpassword start / challenge / verify / update / poll). +/// Parsed outcome of an SSPR interaction step (resetpassword start / challenge / verify / update / poll). /// -/// A single enum represents every HAL interaction response; the validator selects the case +/// A single enum represents every HAL interaction response; the parser selects the case /// from the HAL `state` / `action` pair. -enum MSALNativeAuthV2InteractionValidatedResponse: Equatable { +enum MSALNativeAuthV2InteractionParsedResponse: Equatable { /// `action == challenge`: a verification method is available; the SDK should auto-trigger the challenge. case challengeRequired(continuationToken: String, challengeHref: String, hint: String?) /// `action == verify`: a one-time code is required from the user. diff --git a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift similarity index 94% rename from MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift rename to MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift index 00353570ba..62c40cbcac 100644 --- a/MSAL/src/native_auth/network/responses/v2/validator/MSALNativeAuthV2ResponseValidator.swift +++ b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift @@ -24,26 +24,26 @@ @_implementationOnly import MSAL_Private -/// Maps a raw ``MSALNativeAuthHALResponse`` (or transport error) into a validated, controller-facing response. -protocol MSALNativeAuthV2ResponseValidating { - func validateAuthorizeChallenge( +/// Maps a raw ``MSALNativeAuthHALResponse`` (or transport error) into a parsed, controller-facing response. +protocol MSALNativeAuthV2ResponseParsing { + func parseAuthorizeChallenge( context: MSIDRequestContext, _ result: Result, flowScenario: MSALNativeAuthFlowScenario - ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse - func validateInteraction( + ) -> MSALNativeAuthV2AuthorizeChallengeParsedResponse + func parseInteraction( context: MSIDRequestContext, _ result: Result - ) -> MSALNativeAuthV2InteractionValidatedResponse + ) -> MSALNativeAuthV2InteractionParsedResponse } -final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidating { +final class MSALNativeAuthV2ResponseParser: MSALNativeAuthV2ResponseParsing { - func validateAuthorizeChallenge( + func parseAuthorizeChallenge( context: MSIDRequestContext, _ result: Result, flowScenario: MSALNativeAuthFlowScenario - ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { + ) -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { switch result { case .failure(let error): return .error(flowError(from: error, context: context)) @@ -75,10 +75,10 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin } } - func validateInteraction( + func parseInteraction( context: MSIDRequestContext, _ result: Result - ) -> MSALNativeAuthV2InteractionValidatedResponse { + ) -> MSALNativeAuthV2InteractionParsedResponse { switch result { case .failure(let error): return .error(flowError(from: error, context: context)) @@ -158,7 +158,7 @@ final class MSALNativeAuthV2ResponseValidator: MSALNativeAuthV2ResponseValidatin } } -extension MSALNativeAuthV2ResponseValidator { +extension MSALNativeAuthV2ResponseParser { // MARK: - Error mapping @@ -167,7 +167,7 @@ extension MSALNativeAuthV2ResponseValidator { private func missingLink( _ relation: MSALNativeAuthV2LinkRelation, context: MSIDRequestContext - ) -> MSALNativeAuthV2InteractionValidatedResponse { + ) -> MSALNativeAuthV2InteractionParsedResponse { MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: missing '%@' link", relation.rawValue) return .error(MSALNativeAuthFlowError( type: .generalError, diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index a260df3813..9cececbc72 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -31,7 +31,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { private var sut: MSALNativeAuthFlowController! private var requestProviderMock: MSALNativeAuthV2RequestProviderMock! - private var validatorMock: MSALNativeAuthV2ResponseValidatorMock! + private var parserMock: MSALNativeAuthV2ResponseParserMock! private var cacheAccessorMock: MSALNativeAuthCacheAccessorMock! private var resultFactoryMock: MSALNativeAuthResultFactoryMock! private let context = MSALNativeAuthRequestContext(correlationId: UUID()) @@ -40,14 +40,14 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { try super.setUpWithError() requestProviderMock = .init() - validatorMock = .init() + parserMock = .init() cacheAccessorMock = .init() resultFactoryMock = .init() sut = .init( config: MSALNativeAuthConfigStubs.configuration, requestProvider: requestProviderMock, - responseValidator: validatorMock, + responseParser: parserMock, cacheAccessor: cacheAccessorMock, resultFactory: resultFactoryMock ) @@ -86,10 +86,10 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_happyPath_returnsCodeRequired() async { requestProviderMock.mockRequest() - validatorMock.authorizeChallengeResponses = [ + parserMock.authorizeChallengeResponses = [ .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") ] - validatorMock.interactionResponses = [ + parserMock.interactionResponses = [ .challengeRequired(continuationToken: "ct-2", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com"), .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] @@ -109,7 +109,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_whenAuthorizationChallengeFails_returnsError() async { requestProviderMock.mockRequest() - validatorMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(type: .generalError))] + parserMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(type: .generalError))] let response = await sut.resetPassword(parameters: resetPasswordParameters()) @@ -121,10 +121,10 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_resetPassword_whenUserNotFound_returnsError() async { requestProviderMock.mockRequest() - validatorMock.authorizeChallengeResponses = [ + parserMock.authorizeChallengeResponses = [ .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") ] - validatorMock.interactionResponses = [ + parserMock.interactionResponses = [ .error(MSALNativeAuthFlowError(type: .userNotFound)) ] @@ -140,7 +140,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_submitCode_whenUpdateRequired_returnsNewPasswordRequired() async { requestProviderMock.mockRequest() - validatorMock.interactionResponses = [ + parserMock.interactionResponses = [ .updateRequired(continuationToken: "ct-update", updateHref: "https://contoso.com/update") ] let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) @@ -159,7 +159,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_submitCode_whenInvalidCode_returnsErrorWithRetryState() async { requestProviderMock.mockRequest() - validatorMock.interactionResponses = [ + parserMock.interactionResponses = [ .error(MSALNativeAuthFlowError(type: .invalidCode)) ] let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) @@ -189,11 +189,11 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_submitNewPassword_happyPath_returnsCompleted() async { requestProviderMock.mockRequest() - validatorMock.interactionResponses = [ + parserMock.interactionResponses = [ .pollInProgress(continuationToken: "ct-poll", pollHref: "https://contoso.com/poll"), .readyToComplete(continuationToken: "ct-continue") ] - validatorMock.authorizeChallengeResponses = [ + parserMock.authorizeChallengeResponses = [ .authorizationCode(code: "auth-code") ] cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() @@ -225,7 +225,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { func test_resendCode_whenCodeRequired_returnsCodeRequired() async { requestProviderMock.mockRequest() - validatorMock.interactionResponses = [ + parserMock.interactionResponses = [ .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] let state = makeState(links: ["resend": URL(string: "https://contoso.com/resend")!]) @@ -281,7 +281,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { scopes: [], apiId: .telemetryApiIdResetPassword, event: nil, - context: context() + context: context ) guard case .browserRequired = response.result else { return XCTFail("Expected browserRequired, got \(response.result)") diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseParserMock.swift similarity index 65% rename from MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift rename to MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseParserMock.swift index 92c3b71827..c5543112f0 100644 --- a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseValidatorMock.swift +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseParserMock.swift @@ -26,33 +26,33 @@ import Foundation @testable import MSAL @_implementationOnly import MSAL_Private -class MSALNativeAuthV2ResponseValidatorMock: MSALNativeAuthV2ResponseValidating { +class MSALNativeAuthV2ResponseParserMock: MSALNativeAuthV2ResponseParsing { - var authorizeChallengeResponses: [MSALNativeAuthV2AuthorizeChallengeValidatedResponse] = [] - var interactionResponses: [MSALNativeAuthV2InteractionValidatedResponse] = [] + var authorizeChallengeResponses: [MSALNativeAuthV2AuthorizeChallengeParsedResponse] = [] + var interactionResponses: [MSALNativeAuthV2InteractionParsedResponse] = [] - private(set) var validateAuthorizeChallengeCallCount = 0 - private(set) var validateInteractionCallCount = 0 + private(set) var parseAuthorizeChallengeCallCount = 0 + private(set) var parseInteractionCallCount = 0 - func validateAuthorizeChallenge( + func parseAuthorizeChallenge( context: MSIDRequestContext, _ result: Result, flowScenario: MSALNativeAuthFlowScenario - ) -> MSALNativeAuthV2AuthorizeChallengeValidatedResponse { - defer { validateAuthorizeChallengeCallCount += 1 } - if validateAuthorizeChallengeCallCount < authorizeChallengeResponses.count { - return authorizeChallengeResponses[validateAuthorizeChallengeCallCount] + ) -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { + defer { parseAuthorizeChallengeCallCount += 1 } + if parseAuthorizeChallengeCallCount < authorizeChallengeResponses.count { + return authorizeChallengeResponses[parseAuthorizeChallengeCallCount] } return .error(MSALNativeAuthFlowError(type: .generalError)) } - func validateInteraction( + func parseInteraction( context: MSIDRequestContext, _ result: Result - ) -> MSALNativeAuthV2InteractionValidatedResponse { - defer { validateInteractionCallCount += 1 } - if validateInteractionCallCount < interactionResponses.count { - return interactionResponses[validateInteractionCallCount] + ) -> MSALNativeAuthV2InteractionParsedResponse { + defer { parseInteractionCallCount += 1 } + if parseInteractionCallCount < interactionResponses.count { + return interactionResponses[parseInteractionCallCount] } return .error(MSALNativeAuthFlowError(type: .generalError)) } diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift similarity index 70% rename from MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift rename to MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift index e51da2364e..85b7fad12f 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseValidatorTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift @@ -26,14 +26,14 @@ import XCTest @testable import MSAL @_implementationOnly import MSAL_Private -final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { +final class MSALNativeAuthV2ResponseParserTests: XCTestCase { - private var sut: MSALNativeAuthV2ResponseValidator! + private var sut: MSALNativeAuthV2ResponseParser! private var context: MSALNativeAuthRequestContext! override func setUp() { super.setUp() - sut = MSALNativeAuthV2ResponseValidator() + sut = MSALNativeAuthV2ResponseParser() context = MSALNativeAuthRequestContextMock() } @@ -74,53 +74,53 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { ) } - // MARK: - validateAuthorizeChallenge + // MARK: - parseAuthorizeChallenge - func test_validateAuthorizeChallenge_withContinuationToken() { + func test_parseAuthorizeChallenge_withContinuationToken() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .passwordReset) + let result = sut.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .passwordReset) XCTAssertEqual(result, .continuationToken(continuationToken: "ct", href: "https://contoso.com/reset")) } - func test_validateAuthorizeChallenge_missingFlowLink_returnsError() { + func test_parseAuthorizeChallenge_missingFlowLink_returnsError() { let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) - let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .signUp) + let result = sut.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .signUp) XCTAssertEqual(result, .error(MSALNativeAuthFlowError( type: .generalError, errorDescription: "Invalid authorize-challenge response: missing 'sign_up' link" ))) } - func test_validateAuthorizeChallenge_withAuthorizationCode() { + func test_parseAuthorizeChallenge_withAuthorizationCode() { let response = makeResponse(code: "auth-code") - let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) + let result = sut.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) XCTAssertEqual(result, .authorizationCode(code: "auth-code")) } - func test_validateAuthorizeChallenge_withServerError_returnsError() { + func test_parseAuthorizeChallenge_withServerError_returnsError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) + let result = sut.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - func test_validateAuthorizeChallenge_withTransportFailure_returnsError() { - let result = sut.validateAuthorizeChallenge(context: context, .failure(ErrorMock.error), flowScenario: .signIn) + func test_parseAuthorizeChallenge_withTransportFailure_returnsError() { + let result = sut.parseAuthorizeChallenge(context: context, .failure(ErrorMock.error), flowScenario: .signIn) guard case .error = result else { return XCTFail("Expected error") } } - // MARK: - validateInteraction + // MARK: - parseInteraction - func test_validateInteraction_challengeAction_returnsChallengeRequired() { + func test_parseInteraction_challengeAction_returnsChallengeRequired() { let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) let response = makeResponse(state: "interactionRequired", action: "challenge", continuationToken: "ct", methods: [method]) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .challengeRequired(continuationToken: "ct", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com")) } - func test_validateInteraction_verifyAction_returnsCodeRequired() { + func test_parseInteraction_verifyAction_returnsCodeRequired() { let response = makeResponse( state: "interactionRequired", action: "verify", @@ -129,11 +129,11 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { hint: "u***@contoso.com", links: ["verify": "https://contoso.com/verify", "resend": "https://contoso.com/resend"] ) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8)) } - func test_validateInteraction_verifyAction_usesServerChannelType() { + func test_parseInteraction_verifyAction_usesServerChannelType() { let response = makeResponse( state: "interactionRequired", action: "verify", @@ -143,103 +143,103 @@ final class MSALNativeAuthV2ResponseValidatorTests: XCTestCase { methodType: "sms", links: ["verify": "https://contoso.com/verify", "resend": "https://contoso.com/resend"] ) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .codeRequired(continuationToken: "ct", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "+1 (***) ***-1234", channelType: MSALNativeAuthChannelType(value: "sms"), codeLength: 8)) } - func test_validateInteraction_updateAction_returnsUpdateRequired() { + func test_parseInteraction_updateAction_returnsUpdateRequired() { let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct", links: ["update": "https://contoso.com/update"]) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .updateRequired(continuationToken: "ct", updateHref: "https://contoso.com/update")) } - func test_validateInteraction_pollAction_returnsPollInProgress() { + func test_parseInteraction_pollAction_returnsPollInProgress() { let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct", links: ["poll": "https://contoso.com/poll"]) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .pollInProgress(continuationToken: "ct", pollHref: "https://contoso.com/poll")) } - func test_validateInteraction_updateAction_withoutUpdateLink_failsWithMissingLink() { + func test_parseInteraction_updateAction_withoutUpdateLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct") - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - func test_validateInteraction_pollAction_withoutPollLink_failsWithMissingLink() { + func test_parseInteraction_pollAction_withoutPollLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct") - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - func test_validateInteraction_verifyAction_withoutVerifyLink_failsWithMissingLink() { + func test_parseInteraction_verifyAction_withoutVerifyLink_failsWithMissingLink() { let response = makeResponse(state: "interactionRequired", action: "verify", continuationToken: "ct", codeLength: 8, hint: "u***@contoso.com") - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - func test_validateInteraction_continueState_returnsReadyToComplete() { + func test_parseInteraction_continueState_returnsReadyToComplete() { let response = makeResponse(state: "continue", continuationToken: "ct") - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .readyToComplete(continuationToken: "ct")) } - func test_validateInteraction_webFallbackRequiredState_returnsBrowserRequired() { + func test_parseInteraction_webFallbackRequiredState_returnsBrowserRequired() { let response = makeResponse( state: "webFallbackRequired", continuationToken: "ct", links: ["webFallback": "https://contoso.com/oauth2/v2.0/authorize"] ) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .browserRequired) } - func test_validateInteraction_redirectToWebError_returnsBrowserRequired() { + func test_parseInteraction_redirectToWebError_returnsBrowserRequired() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "redirect_to_web", message: nil, innerErrorCode: nil, correlationId: nil) let response = makeResponse(continuationToken: "ct", error: serverError) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .browserRequired) } - func test_validateInteraction_userNotFound_mapsToUserNotFound() { + func test_parseInteraction_userNotFound_mapsToUserNotFound() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "AADSTS50034 user not found", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .userNotFound))) } - func test_validateInteraction_invalidGrant_mapsToInvalidCode() { + func test_parseInteraction_invalidGrant_mapsToInvalidCode() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidGrant", message: "wrong code", innerErrorCode: nil, correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCode))) } - func test_validateInteraction_invalidContinuationToken_mapsToGeneralError() { + func test_parseInteraction_invalidContinuationToken_mapsToGeneralError() { let serverError = MSALNativeAuthHALResponse.ServerError(code: "invalidRequest", message: "bad token", innerErrorCode: "invalidContinuationToken", correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) } - func test_validateInteraction_passwordTooWeak_mapsToInvalidPassword() { + func test_parseInteraction_passwordTooWeak_mapsToInvalidPassword() { let serverError = MSALNativeAuthHALResponse.ServerError( code: "invalidRequest", message: "AADSTS120002: New password doesn't meet complexity requirements.", innerErrorCode: "passwordTooWeak", correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidPassword))) } - func test_validateInteraction_invalidUserNameOrPassword_mapsToInvalidCredentials() { + func test_parseInteraction_invalidUserNameOrPassword_mapsToInvalidCredentials() { let serverError = MSALNativeAuthHALResponse.ServerError( code: "invalidGrant", message: "AADSTS50126: Error validating credentials.", innerErrorCode: "invalidUserNameOrPassword", correlationId: nil) let response = makeResponse(error: serverError) - let result = sut.validateInteraction(context: context, .success(response)) + let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCredentials))) } } From baf9a91f610fe8165aa010d1c15034324e4478ba Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Tue, 28 Jul 2026 16:27:05 +0100 Subject: [PATCH 76/85] Add todo to MSALNativeAuthV2HrefURLResolver --- .../network/v2/MSALNativeAuthV2HrefURLResolver.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 96ad172088..3e8487bf43 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -31,6 +31,8 @@ import Foundation /// resolver normalises a server href against the configured authority host, and also builds /// URLs for the fixed ``MSALNativeAuthV2Endpoint`` cases. The slice/data-center query /// parameter is appended consistently. + +// TODO: Update based on API changes struct MSALNativeAuthV2HrefURLResolver { private let authorityURL: URL From e75d8abc4bf13f72701fc3456840f2d608a3cbee Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 29 Jul 2026 15:16:18 +0100 Subject: [PATCH 77/85] Make V2 LinkRelation an extensible struct and prune unused relations Convert MSALNativeAuthV2LinkRelation from an enum to a RawRepresentable struct with one constant per extension, so the type stays open for extension without core edits. Remove the enroll, register, activate, and submitAttributes relations that never appear in the observed responses. Update the flow controller tests to key links by typed relation constants instead of raw strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecf13a9d-6bea-4c2f-834e-ef32ed967500 --- .../v2/MSALNativeAuthV2LinkRelation.swift | 45 +++++++++++++------ .../MSALNativeAuthFlowControllerTests.swift | 22 ++++----- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift index 6579c41201..6691b9e04b 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift @@ -27,17 +27,36 @@ import Foundation /// HAL `_links` relation names the SDK follows to advance a Native Auth V2 (server-driven) flow. /// /// Every href the SDK resolves is keyed by one of these relations (top-level `_links` or an -/// embedded method's `_links`). -enum MSALNativeAuthV2LinkRelation: String { - case challenge - case verify - case resend - case update - case poll - case `continue` - case enroll - case register - case activate - case submitAttributes - case `self` +/// embedded method's `_links`). New relations are added as static constants in their own +/// extension, so the type stays closed for modification and open for extension. +struct MSALNativeAuthV2LinkRelation: RawRepresentable, Hashable { + let rawValue: String +} + +extension MSALNativeAuthV2LinkRelation { + static let challenge = Self(rawValue: "challenge") +} + +extension MSALNativeAuthV2LinkRelation { + static let verify = Self(rawValue: "verify") +} + +extension MSALNativeAuthV2LinkRelation { + static let resend = Self(rawValue: "resend") +} + +extension MSALNativeAuthV2LinkRelation { + static let update = Self(rawValue: "update") +} + +extension MSALNativeAuthV2LinkRelation { + static let poll = Self(rawValue: "poll") +} + +extension MSALNativeAuthV2LinkRelation { + static let `continue` = Self(rawValue: "continue") +} + +extension MSALNativeAuthV2LinkRelation { + static let `self` = Self(rawValue: "self") } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 9cececbc72..d6a2917dcd 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -55,7 +55,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { // MARK: - Helpers - private func makeState(links: [String: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowInternalState { + private func makeState(links: [MSALNativeAuthV2LinkRelation: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowInternalState { let continuation = MSALNativeAuthFlowContinuationState( flowScenario: .passwordReset, continuationToken: continuationToken, @@ -67,14 +67,10 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) } - private func relationLinks(_ links: [String: URL]) -> [MSALNativeAuthV2LinkKey: URL] { - var typed: [MSALNativeAuthV2LinkKey: URL] = [:] - for (rawRelation, url) in links { - if let relation = MSALNativeAuthV2LinkRelation(rawValue: rawRelation) { - typed[.relation(relation)] = url - } + private func relationLinks(_ links: [MSALNativeAuthV2LinkRelation: URL]) -> [MSALNativeAuthV2LinkKey: URL] { + links.reduce(into: [:]) { result, entry in + result[.relation(entry.key)] = entry.value } - return typed } private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParametersV2 { @@ -143,7 +139,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { parserMock.interactionResponses = [ .updateRequired(continuationToken: "ct-update", updateHref: "https://contoso.com/update") ] - let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) + let state = makeState(links: [.verify: URL(string: "https://contoso.com/verify")!]) let response = await sut.submitCode("12345678", state: state) @@ -162,7 +158,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { parserMock.interactionResponses = [ .error(MSALNativeAuthFlowError(type: .invalidCode)) ] - let state = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) + let state = makeState(links: [.verify: URL(string: "https://contoso.com/verify")!]) let response = await sut.submitCode("00000000", state: state) @@ -197,7 +193,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { .authorizationCode(code: "auth-code") ] cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() - let state = makeState(links: ["update": URL(string: "https://contoso.com/update")!]) + let state = makeState(links: [.update: URL(string: "https://contoso.com/update")!]) let response = await sut.submitNewPassword("New-Password-1", state: state) @@ -228,7 +224,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { parserMock.interactionResponses = [ .codeRequired(continuationToken: "ct-3", verifyHref: "https://contoso.com/verify", resendHref: "https://contoso.com/resend", sentTo: "u***@contoso.com", channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8) ] - let state = makeState(links: ["resend": URL(string: "https://contoso.com/resend")!]) + let state = makeState(links: [.resend: URL(string: "https://contoso.com/resend")!]) let response = await sut.resendCode(state: state) @@ -313,7 +309,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { } private func mapErrorNewState(type: MSALNativeAuthFlowError.ErrorType) async -> MSALNativeAuthFlowInternalState? { - let recoverableState = makeState(links: ["verify": URL(string: "https://contoso.com/verify")!]) + let recoverableState = makeState(links: [.verify: URL(string: "https://contoso.com/verify")!]) let response = await sut.mapInteraction( .error(MSALNativeAuthFlowError(type: type)), flowScenario: .passwordReset, From 9edc2d3d845bcd741ebd71a95fc4c411d09a92e8 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 29 Jul 2026 15:23:30 +0100 Subject: [PATCH 78/85] Moved all endpoints into one enum --- .../network/MSALNativeAuthEndpoint.swift | 1 + .../network/v2/MSALNativeAuthV2Endpoint.swift | 36 ------------------- .../v2/MSALNativeAuthV2HrefURLResolver.swift | 6 ++-- .../MSALNativeAuthV2RequestTarget.swift | 2 +- 4 files changed, 5 insertions(+), 40 deletions(-) delete mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift diff --git a/MSAL/src/native_auth/network/MSALNativeAuthEndpoint.swift b/MSAL/src/native_auth/network/MSALNativeAuthEndpoint.swift index 0a04994e0b..16d6d89295 100644 --- a/MSAL/src/native_auth/network/MSALNativeAuthEndpoint.swift +++ b/MSAL/src/native_auth/network/MSALNativeAuthEndpoint.swift @@ -39,4 +39,5 @@ enum MSALNativeAuthEndpoint: String, CaseIterable { case resetPasswordComplete = "/resetpassword/v1.0/complete" case resetPasswordSubmit = "/resetpassword/v1.0/submit" case resetpasswordPollCompletion = "/resetpassword/v1.0/poll_completion" + case authorizeChallenge = "/oauth2/v2.0/authorize-challenge" } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift deleted file mode 100644 index a834d915d0..0000000000 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2Endpoint.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// Fixed (non server-driven) endpoints used by the Native Auth V2 flows. -/// -/// V2 is server-driven: most steps follow `_links` hrefs returned by the server. Only -/// the authorization challenge / token exchange have fixed paths the client must know up-front. -enum MSALNativeAuthV2Endpoint: String, CaseIterable { - /// Authorization challenge - case authorizeChallenge = "/oauth2/v2.0/authorize-challenge" - /// Token exchange - case token = "/oauth2/v2.0/token" -} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift index 3e8487bf43..5f540448c8 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -29,7 +29,7 @@ import Foundation /// V2 is server-driven, so most steps follow `_links` hrefs returned by the server. Those /// hrefs may be absolute, or relative/templated (e.g. `{tenant}/api/v0.1/auth/...`). This /// resolver normalises a server href against the configured authority host, and also builds -/// URLs for the fixed ``MSALNativeAuthV2Endpoint`` cases. The slice/data-center query +/// URLs for the fixed ``MSALNativeAuthEndpoint`` cases. The slice/data-center query /// parameter is appended consistently. // TODO: Update based on API changes @@ -48,8 +48,8 @@ struct MSALNativeAuthV2HrefURLResolver { self.dataCenter = dataCenter } - /// Builds the URL for a fixed V2 endpoint by appending its path to the authority. - func url(for endpoint: MSALNativeAuthV2Endpoint) throws -> URL { + /// Builds the URL for a fixed endpoint by appending its path to the authority. + func url(for endpoint: MSALNativeAuthEndpoint) throws -> URL { guard var components = URLComponents(url: authorityURL, resolvingAgainstBaseURL: true) else { throw MSALNativeAuthInternalError.invalidUrl } diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift index bdd366441d..ad97794713 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2RequestTarget.swift @@ -26,7 +26,7 @@ import Foundation /// The destination of a V2 request: either a well-known endpoint or a server-provided HAL `href`. enum MSALNativeAuthV2RequestTarget { - case endpoint(MSALNativeAuthV2Endpoint) + case endpoint(MSALNativeAuthEndpoint) case href(String) func url(resolver: MSALNativeAuthV2HrefURLResolver) throws -> URL { From 010291b5adc8bd0c23b9a65b462233bb9fd22e16 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 29 Jul 2026 17:57:45 +0100 Subject: [PATCH 79/85] Update flow controller to remove mapInteraction --- MSAL/MSAL.xcodeproj/project.pbxproj | 6 - .../MSALNativeAuthFlowContinuationState.swift | 16 +- .../v2/MSALNativeAuthFlowController.swift | 333 ++++++++++-------- .../MSALNativeAuthFlowControllerTests.swift | 78 ++-- 4 files changed, 226 insertions(+), 207 deletions(-) diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index b8f6a5f883..6cdf8b825b 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -100,7 +100,6 @@ 12E2160C2D11D3920000F44C /* AuthorityURLFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */; }; 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */; }; 192F74D7E3825C5CDCF50CEB /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */; }; - 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 1E04572324BD5A7D00444756 /* MSALCacheItemDetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */; }; 1E06CD6524D116F800E3D0E5 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6A206371FC510B500755A51 /* Security.framework */; }; 1E1A2E042256D12F001009ED /* MSALTestAppSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = D61A64B01E5AAC5C0086D120 /* MSALTestAppSettings.m */; }; @@ -407,7 +406,6 @@ 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA561CBA51E9F74E8D74868D /* MSALNativeAuthV2HrefParameters.swift */; }; 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseParser.swift */; }; 2DF4C00B2AF30BB95CE7B38A /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B559C5118AEAA5CC979BC05 /* MSALNativeAuthV2AuthorizeChallengeStartParameters.swift */; }; - 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */; }; 32EB647A08781A29C344ACC6 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */; }; 3302D63AD68B02CE3AD172CE /* MSALNativeAuthFlowInternalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */; }; 33A0542A5B652892314FD6C8 /* MSALNativeAuthFlowResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */; }; @@ -2345,7 +2343,6 @@ 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProviderMock.swift; sourceTree = ""; }; 509588E9A2AE1A919D2AF029 /* MSALNativeAuthStrongAuthRegistrationRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthStrongAuthRegistrationRequiredState.swift; sourceTree = ""; }; 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthFlowControllerResponse.swift; sourceTree = ""; }; - 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2Endpoint.swift; sourceTree = ""; }; 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; 58B81F6524AC59A000E8799E /* MSALTestCacheTokenResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALTestCacheTokenResponse.h; sourceTree = ""; }; 58B81F6E24AC59C600E8799E /* MSALTestCacheTokenResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALTestCacheTokenResponse.m; sourceTree = ""; }; @@ -3235,7 +3232,6 @@ 03D8CB5A8588FA78B92F2A2D /* v2 */ = { isa = PBXGroup; children = ( - 57FABBD171CBBFF1CC857572 /* MSALNativeAuthV2Endpoint.swift */, 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */, 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */, 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */, @@ -7522,7 +7518,6 @@ 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ParsedResponses.swift in Sources */, 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseParser.swift in Sources */, - 1966D780CE54C00B2C6F4D93 /* MSALNativeAuthV2Endpoint.swift in Sources */, F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */, D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */, @@ -7857,7 +7852,6 @@ 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ParsedResponses.swift in Sources */, 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseParser.swift in Sources */, - 318A1D129DC236F38703E3B1 /* MSALNativeAuthV2Endpoint.swift in Sources */, 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */, 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */, diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift index e6b674b369..86f7908197 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -33,34 +33,28 @@ enum MSALNativeAuthV2LinkKey: Hashable { /// Internal continuation context carried by a ``MSALNativeAuthFlowInternalState``. /// -/// Holds the opaque server `continuation_token` and the resolved `_links` hrefs -/// the SDK must follow to advance the server-driven flow. -struct MSALNativeAuthFlowContinuationState { +/// Holds the opaque server `continuation_token` and the resolved `_links` hrefs the SDK must +/// follow to advance the server-driven flow. +class MSALNativeAuthFlowContinuationState { let flowScenario: MSALNativeAuthFlowScenario - let continuationToken: String + let continuationToken: String? let links: [MSALNativeAuthV2LinkKey: URL] let username: String? - let sentToHint: String? - let codeLength: Int? /// Scopes (caller-requested merged with the default OIDC scopes) to request on the final /// `/token` exchange. Threaded through every step. let scopes: [String] init( flowScenario: MSALNativeAuthFlowScenario, - continuationToken: String, + continuationToken: String?, links: [MSALNativeAuthV2LinkKey: URL], username: String?, - sentToHint: String? = nil, - codeLength: Int? = nil, scopes: [String] = [] ) { self.flowScenario = flowScenario self.continuationToken = continuationToken self.links = links self.username = username - self.sentToHint = sentToHint - self.codeLength = codeLength self.scopes = scopes } diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 9f6317d971..91ce4996ff 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -24,6 +24,13 @@ @_implementationOnly import MSAL_Private +/// Per-request telemetry context for a single step of a server-driven flow +struct MSALNativeAuthFlowStepContext { + let apiId: MSALNativeAuthTelemetryApiId + let event: MSIDTelemetryAPIEvent? + let context: MSALNativeAuthRequestContext +} + // swiftlint:disable file_length // swiftlint:disable:next type_body_length final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNativeAuthFlowControlling, MSALNativeAuthTokenRequestHandling { @@ -99,7 +106,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } - guard case .challengeRequired(let challengeContinuationToken, let challengeHref, let hint) = startResult else { + guard case .challengeRequired(let challengeContinuationToken, let challengeHref, _) = startResult else { return interactionFailure(startResult, event: event, context: context, scenario: flowScenario, newState: nil) } @@ -112,30 +119,37 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } - return await mapInteraction( - challengeResult, + let continuation = MSALNativeAuthFlowContinuationState( flowScenario: flowScenario, + continuationToken: challengeContinuationToken, + links: [:], username: parameters.username, - scopes: scopes, - apiId: .telemetryApiIdV2ResetPasswordStart, - event: event, - context: context, - fallbackHint: hint + scopes: scopes ) + let step = MSALNativeAuthFlowStepContext(apiId: .telemetryApiIdV2ResetPasswordStart, event: event, context: context) + return await handleChallengeResult(challengeResult, flowContinuationState: continuation, step: step) } // MARK: - Continuation func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) - let continuation = state.continuation + let flowContinuationState = state.continuation let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) - guard let verifyHref = continuation.link(.verify)?.absoluteString else { + guard let verifyHref = flowContinuationState.link(.verify)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), event: event, - context: context, scenario: continuation.flowScenario + context: context, scenario: flowContinuationState.flowScenario + ) + } + + guard let continuationToken = flowContinuationState.continuationToken else { + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token")), + event: event, + context: context, scenario: flowContinuationState.flowScenario ) } @@ -143,21 +157,13 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati try self.requestProvider.verify( href: verifyHref, otp: code, - continuationToken: continuation.continuationToken, + continuationToken: continuationToken, apiId: .telemetryApiIdV2ResetPasswordSubmitCode, context: context ) } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2ResetPasswordSubmitCode, - event: event, - context: context, - recoverableState: state - ) + let step = MSALNativeAuthFlowStepContext(apiId: .telemetryApiIdV2ResetPasswordSubmitCode, event: event, context: context) + return await handleSubmitCodeResult(result, flowContinuationState: flowContinuationState, step: step, recoverableState: state) } func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { @@ -168,13 +174,21 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmit, context: context) - let continuation = state.continuation + let flowContinuationState = state.continuation - guard let updateHref = continuation.link(.update)?.absoluteString else { + guard let updateHref = flowContinuationState.link(.update)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing update link")), event: event, - context: context, scenario: continuation.flowScenario + context: context, scenario: flowContinuationState.flowScenario + ) + } + + guard let continuationToken = flowContinuationState.continuationToken else { + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token")), + event: event, + context: context, scenario: flowContinuationState.flowScenario ) } @@ -182,14 +196,14 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati try self.requestProvider.updatePassword( href: updateHref, newPassword: password, - continuationToken: continuation.continuationToken, + continuationToken: continuationToken, apiId: .telemetryApiIdV2ResetPasswordSubmit, context: context ) } guard case .pollInProgress(var pollToken, let pollHref) = updateResult else { - return interactionFailure(updateResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) + return interactionFailure(updateResult, event: event, context: context, scenario: flowContinuationState.flowScenario, newState: nil) } let retryExecutor = MSALNativeAuthRetryExecutor(delays: [pollIntervalSeconds]) @@ -222,23 +236,16 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) ), event: event, - context: context, scenario: continuation.flowScenario + context: context, scenario: flowContinuationState.flowScenario ) } guard case .readyToComplete(let completionToken) = terminalPollResult else { - return interactionFailure(terminalPollResult, event: event, context: context, scenario: continuation.flowScenario, newState: nil) + return interactionFailure(terminalPollResult, event: event, context: context, scenario: flowContinuationState.flowScenario, newState: nil) } - return await completeWithToken( - flowScenario: continuation.flowScenario, - continuationToken: completionToken, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2ResetPasswordSubmit, - event: event, - context: context - ) + let step = MSALNativeAuthFlowStepContext(apiId: .telemetryApiIdV2ResetPasswordSubmit, event: event, context: context) + return await completeWithToken(flowContinuationState: flowContinuationState, continuationToken: completionToken, step: step) } func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { @@ -260,35 +267,35 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { let context = MSALNativeAuthRequestContext(correlationId: nil) let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordResendCode, context: context) - let continuation = state.continuation + let flowContinuationState = state.continuation - guard let resendHref = continuation.link(.resend)?.absoluteString else { + guard let resendHref = flowContinuationState.link(.resend)?.absoluteString else { return failure( .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing resend link")), event: event, - context: context, scenario: continuation.flowScenario + context: context, scenario: flowContinuationState.flowScenario + ) + } + + guard let continuationToken = flowContinuationState.continuationToken else { + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token")), + event: event, + context: context, scenario: flowContinuationState.flowScenario ) } let result = await performInteraction(context: context) { try self.requestProvider.challenge( href: resendHref, - continuationToken: continuation.continuationToken, + continuationToken: continuationToken, apiId: .telemetryApiIdV2ResetPasswordResendCode, context: context ) } - return await mapInteraction( - result, - flowScenario: continuation.flowScenario, - username: continuation.username, - scopes: continuation.scopes, - apiId: .telemetryApiIdV2ResetPasswordResendCode, - event: event, - context: context, - fallbackHint: continuation.sentToHint - ) + let step = MSALNativeAuthFlowStepContext(apiId: .telemetryApiIdV2ResetPasswordResendCode, event: event, context: context) + return handleResendCodeResult(result, flowContinuationState: flowContinuationState, step: step) } // MARK: - Shared step helpers @@ -339,116 +346,167 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati // MARK: - Result mapping - // Maps a parsed interaction response onto a controller response, building the next - // required state or, on a terminal response, running the authorize-challenge → token completion. - func mapInteraction( + /// Maps the challenge response from the reset-password start sequence. + func handleChallengeResult( _ result: MSALNativeAuthV2InteractionParsedResponse, - flowScenario: MSALNativeAuthFlowScenario, - username: String?, - scopes: [String], - apiId: MSALNativeAuthTelemetryApiId, - event: MSIDTelemetryAPIEvent?, - context: MSALNativeAuthRequestContext, - recoverableState: MSALNativeAuthFlowInternalState? = nil, - fallbackHint: String? = nil + flowContinuationState: MSALNativeAuthFlowContinuationState, + step: MSALNativeAuthFlowStepContext ) async -> MSALNativeAuthFlowControllerResponse { switch result { + case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let channelType, let codeLength): + let next = makeContinuation(from: flowContinuationState, continuationToken: token, links: [.verify: verifyHref, .resend: resendHref]) + return codeRequiredResponse(flowContinuationState: next, sentTo: sentTo, channelType: channelType, codeLength: codeLength, step: step) case .readyToComplete(let token): - return await completeWithToken( - flowScenario: flowScenario, - continuationToken: token, - username: username, - scopes: scopes, - apiId: apiId, - event: event, - context: context - ) + return await completeWithToken(flowContinuationState: flowContinuationState, continuationToken: token, step: step) + case .browserRequired: + stopTelemetryEvent(step.event, context: step.context) + return response(.browserRequired, context: step.context, scenario: flowContinuationState.flowScenario) + case .error(let error): + stopTelemetryEvent(step.event, context: step.context, error: error) + return response(.error(error: error, newState: nil), context: step.context, scenario: flowContinuationState.flowScenario) + default: + return interactionFailure(result, event: step.event, context: step.context, scenario: flowContinuationState.flowScenario, newState: nil) + } + } + + /// Maps the challenge response produced when the user asks to resend the one-time code. + func handleResendCodeResult( + _ result: MSALNativeAuthV2InteractionParsedResponse, + flowContinuationState: MSALNativeAuthFlowContinuationState, + step: MSALNativeAuthFlowStepContext + ) -> MSALNativeAuthFlowControllerResponse { + switch result { case .codeRequired(let token, let verifyHref, let resendHref, let sentTo, let channelType, let codeLength): - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.verify: verifyHref, .resend: resendHref], - username: username, - sentToHint: sentTo.isEmpty ? fallbackHint : sentTo, - codeLength: codeLength, - scopes: scopes - ) - let displaySentTo = sentTo.isEmpty ? (fallbackHint ?? "") : sentTo - let state = MSALNativeAuthCodeRequiredState(internalState: newState, sentTo: displaySentTo, channel: channelType, codeLength: codeLength) - stopTelemetryEvent(event, context: context) - return response(.actionRequired(state: state), context: context) + let next = makeContinuation(from: flowContinuationState, continuationToken: token, links: [.verify: verifyHref, .resend: resendHref]) + return codeRequiredResponse(flowContinuationState: next, sentTo: sentTo, channelType: channelType, codeLength: codeLength, step: step) + case .browserRequired: + stopTelemetryEvent(step.event, context: step.context) + return response(.browserRequired, context: step.context, scenario: flowContinuationState.flowScenario) + case .error(let error): + stopTelemetryEvent(step.event, context: step.context, error: error) + return response(.error(error: error, newState: nil), context: step.context, scenario: flowContinuationState.flowScenario) + default: + return interactionFailure(result, event: step.event, context: step.context, scenario: flowContinuationState.flowScenario, newState: nil) + } + } + + /// Maps the verify response from submitting a one-time code. + func handleSubmitCodeResult( + _ result: MSALNativeAuthV2InteractionParsedResponse, + flowContinuationState: MSALNativeAuthFlowContinuationState, + step: MSALNativeAuthFlowStepContext, + recoverableState: MSALNativeAuthFlowInternalState? + ) async -> MSALNativeAuthFlowControllerResponse { + switch result { case .updateRequired(let token, let updateHref): - let newState = makeState( - flowScenario, - continuationToken: token, - links: [.update: updateHref], - username: username, - scopes: scopes - ) - stopTelemetryEvent(event, context: context) - return response( - .actionRequired(state: MSALNativeAuthNewPasswordRequiredState(internalState: newState)), - context: context) + let next = makeContinuation(from: flowContinuationState, continuationToken: token, links: [.update: updateHref]) + return newPasswordRequiredResponse(flowContinuationState: next, step: step) + case .readyToComplete(let token): + return await completeWithToken(flowContinuationState: flowContinuationState, continuationToken: token, step: step) + case .browserRequired: + stopTelemetryEvent(step.event, context: step.context) + return response(.browserRequired, context: step.context, scenario: flowContinuationState.flowScenario) case .error(let error): - stopTelemetryEvent(event, context: context, error: error) + stopTelemetryEvent(step.event, context: step.context, error: error) return response( .error(error: error, newState: error.isInvalidCode || error.type == .invalidPassword ? recoverableState : nil), - context: context, scenario: flowScenario + context: step.context, scenario: flowContinuationState.flowScenario ) - case .browserRequired: - stopTelemetryEvent(event, context: context) - return response(.browserRequired, context: context, scenario: flowScenario) default: - return interactionFailure(result, event: event, context: context, scenario: flowScenario, newState: nil) + return interactionFailure(result, event: step.event, context: step.context, scenario: flowContinuationState.flowScenario, newState: nil) } } - /// Completion sequence shared by every flow: authorize-challenge (continue) → token exchange. - /// The `/token` response is persisted to the shared MSAL token cache so the returned - /// ``MSALNativeAuthUserAccountResult`` can retrieve access tokens via `getAccessToken(...)`. + // MARK: - Response builders + + /// Derives the next continuation for a flow step.. + private func makeContinuation( + from flowContinuationState: MSALNativeAuthFlowContinuationState, + continuationToken: String, + links: [MSALNativeAuthV2LinkRelation: String?] + ) -> MSALNativeAuthFlowContinuationState { + return MSALNativeAuthFlowContinuationState( + flowScenario: flowContinuationState.flowScenario, + continuationToken: continuationToken, + links: resolveLinks(links), + username: flowContinuationState.username, + scopes: flowContinuationState.scopes + ) + } + + private func codeRequiredResponse( + flowContinuationState: MSALNativeAuthFlowContinuationState, + sentTo: String, + channelType: MSALNativeAuthChannelType, + codeLength: Int, + step: MSALNativeAuthFlowStepContext + ) -> MSALNativeAuthFlowControllerResponse { + let internalState = MSALNativeAuthFlowInternalState(continuation: flowContinuationState, controller: self) + let state = MSALNativeAuthCodeRequiredState( + internalState: internalState, + sentTo: sentTo, + channel: channelType, + codeLength: codeLength + ) + stopTelemetryEvent(step.event, context: step.context) + return response(.actionRequired(state: state), context: step.context) + } + + private func newPasswordRequiredResponse( + flowContinuationState: MSALNativeAuthFlowContinuationState, + step: MSALNativeAuthFlowStepContext + ) -> MSALNativeAuthFlowControllerResponse { + let internalState = MSALNativeAuthFlowInternalState(continuation: flowContinuationState, controller: self) + stopTelemetryEvent(step.event, context: step.context) + return response( + .actionRequired(state: MSALNativeAuthNewPasswordRequiredState(internalState: internalState)), + context: step.context + ) + } + + /// Completion sequence shared by every flowContinuationState: authorize-challenge (continue) → token exchange. private func completeWithToken( - flowScenario: MSALNativeAuthFlowScenario, + flowContinuationState: MSALNativeAuthFlowContinuationState, continuationToken: String, - username: String?, - scopes: [String], - apiId: MSALNativeAuthTelemetryApiId, - event: MSIDTelemetryAPIEvent?, - context: MSALNativeAuthRequestContext + step: MSALNativeAuthFlowStepContext ) async -> MSALNativeAuthFlowControllerResponse { let codeResult = await performAuthorizeChallengeContinue( - flowScenario: flowScenario, + flowScenario: flowContinuationState.flowScenario, continuationToken: continuationToken, - apiId: apiId, - context: context + apiId: step.apiId, + context: step.context ) guard case .authorizationCode(let code) = codeResult else { - return failure(codeResult, event: event, context: context, scenario: flowScenario) + return failure(codeResult, event: step.event, context: step.context, scenario: flowContinuationState.flowScenario) } - let tokenResponseResult = await performTokenExchange(code: code, scopes: scopes, apiId: apiId, context: context) + let tokenResponseResult = await performTokenExchange(code: code, + scopes: flowContinuationState.scopes, + apiId: step.apiId, + context: step.context) switch tokenResponseResult { case .success(let tokenResponse): do { let msidConfiguration = resultFactory.makeMSIDConfiguration(scopes: retrieveScopes(from: tokenResponse)) - let tokenResult = try cacheTokenResponse(tokenResponse, context: context, msidConfiguration: msidConfiguration) + let tokenResult = try cacheTokenResponse(tokenResponse, context: step.context, msidConfiguration: msidConfiguration) - guard let accountResult = resultFactory.makeUserAccountResult(tokenResult: tokenResult, context: context) else { + guard let accountResult = resultFactory.makeUserAccountResult(tokenResult: tokenResult, context: step.context) else { let error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unable to construct account result") - stopTelemetryEvent(event, context: context, error: error) - return response(.error(error: error, newState: nil), context: context, scenario: flowScenario) + stopTelemetryEvent(step.event, context: step.context, error: error) + return response(.error(error: error, newState: nil), context: step.context, scenario: flowContinuationState.flowScenario) } - stopTelemetryEvent(event, context: context) - return response(.completed(accountResult), context: context, scenario: flowScenario) + stopTelemetryEvent(step.event, context: step.context) + return response(.completed(accountResult), context: step.context, scenario: flowContinuationState.flowScenario) } catch { let flowError = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unable to save tokens to the cache") - stopTelemetryEvent(event, context: context, error: flowError) - return response(.error(error: flowError, newState: nil), context: context, scenario: flowScenario) + stopTelemetryEvent(step.event, context: step.context, error: flowError) + return response(.error(error: flowError, newState: nil), context: step.context, scenario: flowContinuationState.flowScenario) } case .failure(let error): let flowError = (error as? MSALNativeAuthFlowError) ?? MSALNativeAuthFlowError(type: .generalError, errorDescription: (error as NSError).localizedDescription) - stopTelemetryEvent(event, context: context, error: flowError) - return response(.error(error: flowError, newState: nil), context: context, scenario: flowScenario) + stopTelemetryEvent(step.event, context: step.context, error: flowError) + return response(.error(error: flowError, newState: nil), context: step.context, scenario: flowContinuationState.flowScenario) } } @@ -509,15 +567,9 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati return scope.components(separatedBy: " ").filter { !$0.isEmpty } } - private func makeState( - _ flowScenario: MSALNativeAuthFlowScenario, - continuationToken: String, - links: [MSALNativeAuthV2LinkRelation: String?], - username: String?, - sentToHint: String? = nil, - codeLength: Int? = nil, - scopes: [String] = [] - ) -> MSALNativeAuthFlowInternalState { + /// Resolves server-provided `_links` hrefs into absolute URLs, dropping any that are missing + /// or cannot be resolved. + private func resolveLinks(_ links: [MSALNativeAuthV2LinkRelation: String?]) -> [MSALNativeAuthV2LinkKey: URL] { let resolver = MSALNativeAuthV2HrefURLResolver(config: config) var resolvedLinks: [MSALNativeAuthV2LinkKey: URL] = [:] for (relation, href) in links { @@ -525,16 +577,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati resolvedLinks[.relation(relation)] = url } } - let continuation = MSALNativeAuthFlowContinuationState( - flowScenario: flowScenario, - continuationToken: continuationToken, - links: resolvedLinks, - username: username, - sentToHint: sentToHint, - codeLength: codeLength, - scopes: scopes - ) - return MSALNativeAuthFlowInternalState(continuation: continuation, controller: self) + return resolvedLinks } // MARK: - Response construction diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index d6a2917dcd..005b37af6a 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -60,9 +60,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { flowScenario: .passwordReset, continuationToken: continuationToken, links: relationLinks(links), - username: "user@contoso.com", - sentToHint: "u***@contoso.com", - codeLength: 8 + username: "user@contoso.com" ) return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) } @@ -237,55 +235,37 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { XCTAssertTrue(requestProviderMock.challengeCalled) } - // MARK: - mapInteraction (branch logic) + // MARK: - result handlers (branch logic) - func test_mapInteraction_codeRequired_usesServerSentTo_whenPresent() async { - let state = await mapCodeRequired(sentTo: "u***@contoso.com", fallbackHint: "fallback@contoso.com") + func test_handleChallenge_codeRequired_usesServerSentTo() async { + let state = await mapCodeRequired(sentTo: "u***@contoso.com") XCTAssertEqual(state?.sentTo, "u***@contoso.com") } - func test_mapInteraction_codeRequired_fallsBackToHint_whenServerSentToEmpty() async { - let state = await mapCodeRequired(sentTo: "", fallbackHint: "fallback@contoso.com") - XCTAssertEqual(state?.sentTo, "fallback@contoso.com") - } - - func test_mapInteraction_codeRequired_emptyServerSentToAndNoHint_yieldsEmptyDisplay() async { - let state = await mapCodeRequired(sentTo: "", fallbackHint: nil) - XCTAssertEqual(state?.sentTo, "") - } - - func test_mapInteraction_error_invalidCode_isRecoverable() async { + func test_handleSubmitCode_error_invalidCode_isRecoverable() async { let newState = await mapErrorNewState(type: .invalidCode) XCTAssertNotNil(newState) } - func test_mapInteraction_error_invalidPassword_isRecoverable() async { + func test_handleSubmitCode_error_invalidPassword_isRecoverable() async { let newState = await mapErrorNewState(type: .invalidPassword) XCTAssertNotNil(newState) } - func test_mapInteraction_error_generalError_isNotRecoverable() async { + func test_handleSubmitCode_error_generalError_isNotRecoverable() async { let newState = await mapErrorNewState(type: .generalError) XCTAssertNil(newState) } - func test_mapInteraction_browserRequired_returnsBrowserRequiredResult() async { - let response = await sut.mapInteraction( - .browserRequired, - flowScenario: .passwordReset, - username: "user@contoso.com", - scopes: [], - apiId: .telemetryApiIdResetPassword, - event: nil, - context: context - ) + func test_handleChallenge_browserRequired_returnsBrowserRequiredResult() async { + let response = await sut.handleChallengeResult(.browserRequired, flow: makeFlow(), step: makeStep()) guard case .browserRequired = response.result else { return XCTFail("Expected browserRequired, got \(response.result)") } } - private func mapCodeRequired(sentTo: String, fallbackHint: String?) async -> MSALNativeAuthCodeRequiredState? { - let response = await sut.mapInteraction( + private func mapCodeRequired(sentTo: String) async -> MSALNativeAuthCodeRequiredState? { + let response = await sut.handleChallengeResult( .codeRequired( continuationToken: "ct", verifyHref: "https://contoso.com/verify", @@ -294,13 +274,8 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8 ), - flowScenario: .passwordReset, - username: "user@contoso.com", - scopes: [], - apiId: .telemetryApiIdResetPassword, - event: nil, - context: context, - fallbackHint: fallbackHint + flow: makeFlow(), + step: makeStep() ) guard case .actionRequired(let state) = response.result else { return nil @@ -310,14 +285,10 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { private func mapErrorNewState(type: MSALNativeAuthFlowError.ErrorType) async -> MSALNativeAuthFlowInternalState? { let recoverableState = makeState(links: [.verify: URL(string: "https://contoso.com/verify")!]) - let response = await sut.mapInteraction( + let response = await sut.handleSubmitCodeResult( .error(MSALNativeAuthFlowError(type: type)), - flowScenario: .passwordReset, - username: "user@contoso.com", - scopes: [], - apiId: .telemetryApiIdResetPassword, - event: nil, - context: context, + flow: makeFlow(), + step: makeStep(), recoverableState: recoverableState ) guard case .error(_, let newState) = response.result else { @@ -326,4 +297,21 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { return newState } + private func makeFlow() -> MSALNativeAuthFlowContinuationState { + return MSALNativeAuthFlowContinuationState( + flowScenario: .passwordReset, + continuationToken: "ct", + links: [:], + username: "user@contoso.com" + ) + } + + private func makeStep() -> MSALNativeAuthFlowStepContext { + return MSALNativeAuthFlowStepContext( + apiId: .telemetryApiIdResetPassword, + event: nil, + context: context + ) + } + } From 9e04a46128141c0a3b0ff15bade458c05c8f2a09 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 29 Jul 2026 18:52:05 +0100 Subject: [PATCH 80/85] removed TokenCacher and moved to MSALNativeAuthCacheInterface fixed endpoints --- MSAL/MSAL.xcodeproj/project.pbxproj | 6 -- .../cache/MSALNativeAuthCacheInterface.swift | 70 ++++++++++++ .../MSALNativeAuthTokenCacher.swift | 102 ------------------ .../MSALNativeAuthTokenController.swift | 6 +- .../v2/MSALNativeAuthFlowController.swift | 6 +- .../MSALNativeAuthFlowControllerTests.swift | 6 +- .../network/MSALNativeAuthEndpointTests.swift | 6 +- 7 files changed, 84 insertions(+), 118 deletions(-) delete mode 100644 MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 6cdf8b825b..95bcfc89a8 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -427,7 +427,6 @@ 547D9B6A1EA16110560F531F /* MSALNativeAuthV2ParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */; }; 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */; }; 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */; }; - 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; 583BFD0F24DC8E670035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; 583BFD1024DC8EE80035B901 /* MSALRedirectUriVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E07B0210E542C007E3A3C /* MSALRedirectUriVerifier.m */; }; 583BFD1624DDF9B10035B901 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 583BFD1524DDF9B10035B901 /* Launch Screen.storyboard */; }; @@ -1781,7 +1780,6 @@ E952116ECFF2C75D77A896AB /* MSALNativeAuthFlowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */; }; F05FC2CFEF1AE5462086AD0C /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */; }; - F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */; }; F68F10EB13E4A78906E6C12E /* MSALNativeAuthAttributesInvalidState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEF89FFAE159B6EE80EDFC7 /* MSALNativeAuthAttributesInvalidState.swift */; }; F819D42E8772D0CDAB08945A /* MSALNativeAuthFlowResponseDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */; }; F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */; }; @@ -2479,7 +2477,6 @@ A0274CDA24B54A7000BD198D /* MSALDevicePopManagerUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDevicePopManagerUtil.h; sourceTree = ""; }; A315CA10DE2299B7370E11BE /* MSALNativeAuthAttributesRequiredState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthAttributesRequiredState.swift; sourceTree = ""; }; B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestProvider.swift; sourceTree = ""; }; - B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthTokenCacher.swift; sourceTree = ""; }; B203459221AF77FB00B221AA /* MSALRedirectUri.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALRedirectUri.h; sourceTree = ""; }; B203459321AF77FB00B221AA /* MSALRedirectUri.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALRedirectUri.m; sourceTree = ""; }; B203459C21AFA1FB00B221AA /* MSALRedirectUri+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALRedirectUri+Internal.h"; sourceTree = ""; }; @@ -3284,7 +3281,6 @@ DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */, E2EFAD152A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift */, 1718011BA0E3C2154663D13F /* v2 */, - B1A49A329615FF80BA035F06 /* MSALNativeAuthTokenCacher.swift */, 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */, ); path = controllers; @@ -7531,7 +7527,6 @@ 76ACA3209E92AFC0CD4988B0 /* MSALNativeAuthV2TokenParameters.swift in Sources */, 7B9A32EA8EE3F6A20D0CFA80 /* MSALNativeAuthV2EntryParameters.swift in Sources */, 7662552749019C91197EA86B /* MSALNativeAuthV2HrefParameters.swift in Sources */, - 5787393595522838D3EE3618 /* MSALNativeAuthTokenCacher.swift in Sources */, E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */, ); @@ -7865,7 +7860,6 @@ 4650C74D5FAF055CFBBD879E /* MSALNativeAuthV2TokenParameters.swift in Sources */, C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */, 2C9565109EC22ADD383D36B2 /* MSALNativeAuthV2HrefParameters.swift in Sources */, - F41ACB8F30F666D544798A51 /* MSALNativeAuthTokenCacher.swift in Sources */, D661864E47CDC268A2FA5EEF /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */, ); diff --git a/MSAL/src/native_auth/cache/MSALNativeAuthCacheInterface.swift b/MSAL/src/native_auth/cache/MSALNativeAuthCacheInterface.swift index 85a3f94b8f..42705512ab 100644 --- a/MSAL/src/native_auth/cache/MSALNativeAuthCacheInterface.swift +++ b/MSAL/src/native_auth/cache/MSALNativeAuthCacheInterface.swift @@ -52,3 +52,73 @@ protocol MSALNativeAuthCacheInterface { clientId: String, context: MSIDRequestContext) throws } + +extension MSALNativeAuthCacheInterface { + + func cache( + _ tokenResponse: MSIDTokenResponse, + context: MSIDRequestContext, + msidConfiguration: MSIDConfiguration, + validateAccount: (_ tokenResult: MSIDTokenResult, _ accountIdentifier: MSIDAccountIdentifier) throws -> Bool + ) throws -> MSIDTokenResult { + let displayableId = tokenResponse.idTokenObj?.username() + let homeAccountId = tokenResponse.idTokenObj?.userId + + guard let accountIdentifier = MSIDAccountIdentifier(displayableId: displayableId, homeAccountId: homeAccountId) else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error creating account identifier") + throw MSALNativeAuthInternalError.invalidResponse + } + + // Remove any existing account for this configuration before saving the new tokens. + clearExistingAccount(msidConfiguration: msidConfiguration, context: context) + + let savedResult: MSIDTokenResult? + do { + savedResult = try validateAndSaveTokensAndAccount( + tokenResponse: tokenResponse, + configuration: msidConfiguration, + context: context + ) + } catch { + MSALNativeAuthLogger.logPII( + level: .warning, + context: context, + format: "Error caching response: \(MSALLogMask.maskEUII(error)) (ignoring)") + savedResult = nil + } + + guard let result = savedResult else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error caching token response") + throw MSALNativeAuthInternalError.invalidResponse + } + + guard try validateAccount(result, accountIdentifier) else { + MSALNativeAuthLogger.log(level: .error, context: context, format: "Error validating account") + throw MSALNativeAuthInternalError.invalidResponse + } + + return result + } + + private func clearExistingAccount(msidConfiguration: MSIDConfiguration, context: MSIDRequestContext) { + do { + let accounts = try getAllAccounts(configuration: msidConfiguration) + if let account = accounts.first { + if let identifier = MSIDAccountIdentifier(displayableId: account.username, homeAccountId: account.identifier) { + try clearCache( + accountIdentifier: identifier, + authority: msidConfiguration.authority, + clientId: msidConfiguration.clientId, + context: context) + } + } else { + MSALNativeAuthLogger.log( + level: .warning, + context: context, + format: "Error creating MSIDAccountIdentifier out of MSALAccount (ignoring)") + } + } catch { + MSALNativeAuthLogger.log(level: .warning, context: context, format: "Error clearing previous account (ignoring)") + } + } +} diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift deleted file mode 100644 index 24a34a11df..0000000000 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenCacher.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// All rights reserved. -// -// This code is licensed under the MIT License. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files(the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions : -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -@_implementationOnly import MSAL_Private - -/// Token-response cache persistence. -final class MSALNativeAuthTokenCacher { - - private let cacheAccessor: MSALNativeAuthCacheInterface - - init(cacheAccessor: MSALNativeAuthCacheInterface) { - self.cacheAccessor = cacheAccessor - } - - func cache( - _ tokenResponse: MSIDTokenResponse, - context: MSIDRequestContext, - msidConfiguration: MSIDConfiguration, - validateAccount: (_ tokenResult: MSIDTokenResult, _ accountIdentifier: MSIDAccountIdentifier) throws -> Bool - ) throws -> MSIDTokenResult { - let displayableId = tokenResponse.idTokenObj?.username() - let homeAccountId = tokenResponse.idTokenObj?.userId - - guard let accountIdentifier = MSIDAccountIdentifier(displayableId: displayableId, homeAccountId: homeAccountId) else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error creating account identifier") - throw MSALNativeAuthInternalError.invalidResponse - } - - // Remove any existing account for this configuration before saving the new tokens. - clearExistingAccount(msidConfiguration: msidConfiguration, context: context) - - let savedResult: MSIDTokenResult? - do { - savedResult = try cacheAccessor.validateAndSaveTokensAndAccount( - tokenResponse: tokenResponse, - configuration: msidConfiguration, - context: context - ) - } catch { - MSALNativeAuthLogger.logPII( - level: .warning, - context: context, - format: "Error caching response: \(MSALLogMask.maskEUII(error)) (ignoring)") - savedResult = nil - } - - guard let result = savedResult else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error caching token response") - throw MSALNativeAuthInternalError.invalidResponse - } - - guard try validateAccount(result, accountIdentifier) else { - MSALNativeAuthLogger.log(level: .error, context: context, format: "Error validating account") - throw MSALNativeAuthInternalError.invalidResponse - } - - return result - } - - private func clearExistingAccount(msidConfiguration: MSIDConfiguration, context: MSIDRequestContext) { - do { - let accounts = try cacheAccessor.getAllAccounts(configuration: msidConfiguration) - if let account = accounts.first { - if let identifier = MSIDAccountIdentifier(displayableId: account.username, homeAccountId: account.identifier) { - try cacheAccessor.clearCache( - accountIdentifier: identifier, - authority: msidConfiguration.authority, - clientId: msidConfiguration.clientId, - context: context) - } - } else { - MSALNativeAuthLogger.log( - level: .warning, - context: context, - format: "Error creating MSIDAccountIdentifier out of MSALAccount (ignoring)") - } - } catch { - MSALNativeAuthLogger.log(level: .warning, context: context, format: "Error clearing previous account (ignoring)") - } - } -} diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift index a1e75aeb87..ed3670049a 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift @@ -33,7 +33,7 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController, MSALNativeAut let factory: MSALNativeAuthResultBuildable private let requestProvider: MSALNativeAuthTokenRequestProviding private let responseValidator: MSALNativeAuthTokenResponseValidating - private let tokenCacher: MSALNativeAuthTokenCacher + private let cacheAccessor: MSALNativeAuthCacheInterface init( clientId: String, @@ -45,7 +45,7 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController, MSALNativeAut self.requestProvider = requestProvider self.factory = factory self.responseValidator = responseValidator - self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) + self.cacheAccessor = cacheAccessor super.init( clientId: clientId ) @@ -122,7 +122,7 @@ class MSALNativeAuthTokenController: MSALNativeAuthBaseController, MSALNativeAut context: MSIDRequestContext, msidConfiguration: MSIDConfiguration ) throws -> MSIDTokenResult { - return try tokenCacher.cache( + return try cacheAccessor.cache( tokenResponse, context: context, msidConfiguration: msidConfiguration diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 91ce4996ff..9af1bdeac5 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -39,7 +39,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati private let requestProvider: MSALNativeAuthV2RequestProviding private let responseParser: MSALNativeAuthV2ResponseParsing private let resultFactory: MSALNativeAuthResultBuildable - private let tokenCacher: MSALNativeAuthTokenCacher + private let cacheAccessor: MSALNativeAuthCacheInterface private let kNumberOfTimesToRetryPollCompletionCall = 5 // TODO: Confirm this is needed and server doesn't send @@ -56,7 +56,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati self.requestProvider = requestProvider self.responseParser = responseParser self.resultFactory = resultFactory - self.tokenCacher = MSALNativeAuthTokenCacher(cacheAccessor: cacheAccessor) + self.cacheAccessor = cacheAccessor super.init(clientId: config.clientId) } @@ -532,7 +532,7 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati context: MSALNativeAuthRequestContext, msidConfiguration: MSIDConfiguration ) throws -> MSIDTokenResult { - return try tokenCacher.cache( + return try cacheAccessor.cache( tokenResponse, context: context, msidConfiguration: msidConfiguration diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 005b37af6a..852bfd718f 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -258,7 +258,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { } func test_handleChallenge_browserRequired_returnsBrowserRequiredResult() async { - let response = await sut.handleChallengeResult(.browserRequired, flow: makeFlow(), step: makeStep()) + let response = await sut.handleChallengeResult(.browserRequired, flowContinuationState: makeFlow(), step: makeStep()) guard case .browserRequired = response.result else { return XCTFail("Expected browserRequired, got \(response.result)") } @@ -274,7 +274,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { channelType: MSALNativeAuthChannelType(value: "email"), codeLength: 8 ), - flow: makeFlow(), + flowContinuationState: makeFlow(), step: makeStep() ) guard case .actionRequired(let state) = response.result else { @@ -287,7 +287,7 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { let recoverableState = makeState(links: [.verify: URL(string: "https://contoso.com/verify")!]) let response = await sut.handleSubmitCodeResult( .error(MSALNativeAuthFlowError(type: type)), - flow: makeFlow(), + flowContinuationState: makeFlow(), step: makeStep(), recoverableState: recoverableState ) diff --git a/MSAL/test/unit/native_auth/network/MSALNativeAuthEndpointTests.swift b/MSAL/test/unit/native_auth/network/MSALNativeAuthEndpointTests.swift index b1a43f93ac..1d89950fca 100644 --- a/MSAL/test/unit/native_auth/network/MSALNativeAuthEndpointTests.swift +++ b/MSAL/test/unit/native_auth/network/MSALNativeAuthEndpointTests.swift @@ -30,7 +30,7 @@ final class MSALNativeAuthEndpointTests: XCTestCase { private typealias sut = MSALNativeAuthEndpoint func test_allEndpoints_are_tested() { - XCTAssertEqual(sut.allCases.count, 16) + XCTAssertEqual(sut.allCases.count, 17) } func test_signUp_start() { @@ -92,4 +92,8 @@ final class MSALNativeAuthEndpointTests: XCTestCase { func test_resetPasswordComplete_endpoint() { XCTAssertEqual(sut.resetPasswordComplete.rawValue, "/resetpassword/v1.0/complete") } + + func test_authorizeChallenge_endpoint() { + XCTAssertEqual(sut.authorizeChallenge.rawValue, "/oauth2/v2.0/authorize-challenge") + } } From 77bbc6caa20f1c51281177799ee854a90e6eda65 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 29 Jul 2026 19:16:53 +0100 Subject: [PATCH 81/85] Split MSALNativeAuthV2RequestBody into per request --- MSAL/MSAL.xcodeproj/project.pbxproj | 24 +++++++++++ ...MSALNativeAuthV2ChallengeRequestBody.swift | 27 +++++++++++++ .../v2/MSALNativeAuthV2PollRequestBody.swift | 27 +++++++++++++ .../v2/MSALNativeAuthV2RequestBody.swift | 23 +++++------ .../v2/MSALNativeAuthV2RequestProvider.swift | 8 ++-- ...ativeAuthV2UpdatePasswordRequestBody.swift | 40 +++++++++++++++++++ .../MSALNativeAuthV2VerifyRequestBody.swift | 40 +++++++++++++++++++ .../MSALNativeAuthV2HrefParameters.swift | 3 +- .../v2/MSALNativeAuthV2ParametersTests.swift | 4 +- 9 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2ChallengeRequestBody.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2PollRequestBody.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2UpdatePasswordRequestBody.swift create mode 100644 MSAL/src/native_auth/network/v2/MSALNativeAuthV2VerifyRequestBody.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 95bcfc89a8..050d32303e 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -452,6 +452,10 @@ 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; + 584F147D25DE469499C2B538 /* MSALNativeAuthV2ChallengeRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02F30CED3374A7C81A93A4F /* MSALNativeAuthV2ChallengeRequestBody.swift */; }; + 59B764D2DDE4420295E78F3C /* MSALNativeAuthV2PollRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729C79DAE4CA439EA155E210 /* MSALNativeAuthV2PollRequestBody.swift */; }; + C8C7202B3FCA487AA2E68705 /* MSALNativeAuthV2VerifyRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = D862978DEC304DD299125F27 /* MSALNativeAuthV2VerifyRequestBody.swift */; }; + 88C90FA7417644029A718568 /* MSALNativeAuthV2UpdatePasswordRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC8B004B3BB485B8FA4CB5D /* MSALNativeAuthV2UpdatePasswordRequestBody.swift */; }; 6FF4FECB6AE1581341C3AF5E /* MSALNativeAuthFlowError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */; }; 7207E6302FA58969008F6803 /* MSALDeviceTokenParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 7233F07E2F885A4A009C9602 /* MSALDeviceTokenParameters.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7207E6392FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -488,6 +492,10 @@ 827CE360F94F0A5BCA875193 /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */; }; 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */; }; 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; + CD40769A3ADA4D4A887055D3 /* MSALNativeAuthV2ChallengeRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02F30CED3374A7C81A93A4F /* MSALNativeAuthV2ChallengeRequestBody.swift */; }; + 97E553EE2AA241AE902BC091 /* MSALNativeAuthV2PollRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729C79DAE4CA439EA155E210 /* MSALNativeAuthV2PollRequestBody.swift */; }; + EF61121B295842DC8C240C1E /* MSALNativeAuthV2VerifyRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = D862978DEC304DD299125F27 /* MSALNativeAuthV2VerifyRequestBody.swift */; }; + 92C040B9010549CB8F7149FF /* MSALNativeAuthV2UpdatePasswordRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC8B004B3BB485B8FA4CB5D /* MSALNativeAuthV2UpdatePasswordRequestBody.swift */; }; 886F515829CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; 886F515929CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; settings = {ATTRIBUTES = (Public, ); }; }; 886F515A29CCA50300F09471 /* MSALCIAMAuthority.h in Headers */ = {isa = PBXBuildFile; fileRef = 886F515729CCA50300F09471 /* MSALCIAMAuthority.h */; }; @@ -2150,6 +2158,10 @@ 12E2160A2D11D3920000F44C /* AuthorityURLFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorityURLFormat.swift; sourceTree = ""; }; 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2HrefURLResolverTests.swift; sourceTree = ""; }; 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2RequestBody.swift; sourceTree = ""; }; + A02F30CED3374A7C81A93A4F /* MSALNativeAuthV2ChallengeRequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ChallengeRequestBody.swift; sourceTree = ""; }; + 729C79DAE4CA439EA155E210 /* MSALNativeAuthV2PollRequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2PollRequestBody.swift; sourceTree = ""; }; + D862978DEC304DD299125F27 /* MSALNativeAuthV2VerifyRequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2VerifyRequestBody.swift; sourceTree = ""; }; + 1DC8B004B3BB485B8FA4CB5D /* MSALNativeAuthV2UpdatePasswordRequestBody.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2UpdatePasswordRequestBody.swift; sourceTree = ""; }; 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ParsedResponses.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthV2ParsedResponses.swift; sourceTree = ""; }; 1E04571F24BD5A7D00444756 /* MSALCacheItemDetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALCacheItemDetailViewController.h; sourceTree = ""; }; 1E04572024BD5A7D00444756 /* MSALCacheItemDetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALCacheItemDetailViewController.m; sourceTree = ""; }; @@ -3232,6 +3244,10 @@ 0D259B6FA6FA078E5D941A12 /* MSALNativeAuthV2LinkRelation.swift */, 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */, 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */, + A02F30CED3374A7C81A93A4F /* MSALNativeAuthV2ChallengeRequestBody.swift */, + 729C79DAE4CA439EA155E210 /* MSALNativeAuthV2PollRequestBody.swift */, + D862978DEC304DD299125F27 /* MSALNativeAuthV2VerifyRequestBody.swift */, + 1DC8B004B3BB485B8FA4CB5D /* MSALNativeAuthV2UpdatePasswordRequestBody.swift */, F18852980FF1EB62CED58B88 /* MSALNativeAuthV2HrefURLResolver.swift */, B0BC76AE7D25569C4CF9C167 /* MSALNativeAuthV2RequestProvider.swift */, D2A1F0C4B5E6A7B8C9D0E1F2 /* MSALNativeAuthV2RequestConfigurator.swift */, @@ -7517,6 +7533,10 @@ F20BDF3E3E13BBB47104DCD9 /* MSALNativeAuthV2LinkRelation.swift in Sources */, D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, 8653D7D0AC962C0073333CDC /* MSALNativeAuthV2RequestBody.swift in Sources */, + CD40769A3ADA4D4A887055D3 /* MSALNativeAuthV2ChallengeRequestBody.swift in Sources */, + 97E553EE2AA241AE902BC091 /* MSALNativeAuthV2PollRequestBody.swift in Sources */, + EF61121B295842DC8C240C1E /* MSALNativeAuthV2VerifyRequestBody.swift in Sources */, + 92C040B9010549CB8F7149FF /* MSALNativeAuthV2UpdatePasswordRequestBody.swift in Sources */, 564AB0A43B9671347F1E83A1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 189077057FE38C5C260A2E04 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E101 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, @@ -7850,6 +7870,10 @@ 026328B2E3D999D2224CA191 /* MSALNativeAuthV2LinkRelation.swift in Sources */, 2767F5DC702BBF343C782E1E /* MSALNativeAuthV2RequestBodyKey.swift in Sources */, 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */, + 584F147D25DE469499C2B538 /* MSALNativeAuthV2ChallengeRequestBody.swift in Sources */, + 59B764D2DDE4420295E78F3C /* MSALNativeAuthV2PollRequestBody.swift in Sources */, + C8C7202B3FCA487AA2E68705 /* MSALNativeAuthV2VerifyRequestBody.swift in Sources */, + 88C90FA7417644029A718568 /* MSALNativeAuthV2UpdatePasswordRequestBody.swift in Sources */, 7E475EF1BD0DBD5E66BED4C1 /* MSALNativeAuthV2HrefURLResolver.swift in Sources */, 7D1ED8DBB108BB3F25619C96 /* MSALNativeAuthV2RequestProvider.swift in Sources */, D2A1F0C4B5E6A7B8C9D0E102 /* MSALNativeAuthV2RequestConfigurator.swift in Sources */, diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2ChallengeRequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2ChallengeRequestBody.swift new file mode 100644 index 0000000000..43814e899a --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2ChallengeRequestBody.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthV2ChallengeRequestBody: MSALNativeAuthV2RequestBody {} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2PollRequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2PollRequestBody.swift new file mode 100644 index 0000000000..50e13ea738 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2PollRequestBody.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthV2PollRequestBody: MSALNativeAuthV2RequestBody {} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift index ac4507bd5b..ebe04310aa 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift @@ -24,22 +24,19 @@ import Foundation -/// Typed body of a Native Auth V2 (HAL) follow-up request. Each field maps to a -/// ``MSALNativeAuthV2RequestBodyKey`` and is emitted only when set, so a caller states just the -/// fields the operation carries -struct MSALNativeAuthV2RequestBody { - let continuationToken: String - var otp: String? - var newPassword: String? +/// Base body of a Native Auth V2 (HAL) follow-up request. +class MSALNativeAuthV2RequestBody { + let continuationToken: String? + + init(continuationToken: String?) { + self.continuationToken = continuationToken + } var dictionary: [String: Any] { - var body: [String: Any] = [MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: continuationToken] + var body: [String: Any] = [:] - if let otp = otp { - body[MSALNativeAuthV2RequestBodyKey.otp.rawValue] = otp - } - if let newPassword = newPassword { - body[MSALNativeAuthV2RequestBodyKey.newPassword.rawValue] = newPassword + if let continuationToken = continuationToken { + body[MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue] = continuationToken } return body diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift index 942d651f47..0336257f49 100644 --- a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -120,7 +120,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: apiId, operationType: MSALNativeAuthV2OperationType.challenge.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken) + requestBody: MSALNativeAuthV2ChallengeRequestBody(continuationToken: continuationToken) )) } @@ -136,7 +136,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: apiId, operationType: MSALNativeAuthV2OperationType.verify.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, otp: otp) + requestBody: MSALNativeAuthV2VerifyRequestBody(continuationToken: continuationToken, otp: otp) )) } @@ -152,7 +152,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "PUT", apiId: apiId, operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken, newPassword: newPassword) + requestBody: MSALNativeAuthV2UpdatePasswordRequestBody(continuationToken: continuationToken, newPassword: newPassword) )) } @@ -167,7 +167,7 @@ final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { httpMethod: "POST", apiId: apiId, operationType: MSALNativeAuthV2OperationType.poll.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: continuationToken) + requestBody: MSALNativeAuthV2PollRequestBody(continuationToken: continuationToken) )) } diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2UpdatePasswordRequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2UpdatePasswordRequestBody.swift new file mode 100644 index 0000000000..13a46780dd --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2UpdatePasswordRequestBody.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthV2UpdatePasswordRequestBody: MSALNativeAuthV2RequestBody { + let newPassword: String + + init(continuationToken: String?, newPassword: String) { + self.newPassword = newPassword + super.init(continuationToken: continuationToken) + } + + override var dictionary: [String: Any] { + var body = super.dictionary + body[MSALNativeAuthV2RequestBodyKey.newPassword.rawValue] = newPassword + return body + } +} diff --git a/MSAL/src/native_auth/network/v2/MSALNativeAuthV2VerifyRequestBody.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2VerifyRequestBody.swift new file mode 100644 index 0000000000..063a207a85 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2VerifyRequestBody.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthV2VerifyRequestBody: MSALNativeAuthV2RequestBody { + let otp: String + + init(continuationToken: String?, otp: String) { + self.otp = otp + super.init(continuationToken: continuationToken) + } + + override var dictionary: [String: Any] { + var body = super.dictionary + body[MSALNativeAuthV2RequestBodyKey.otp.rawValue] = otp + return body + } +} diff --git a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift index 27595f3fa0..d83190e049 100644 --- a/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift @@ -24,8 +24,7 @@ import Foundation -/// A HAL follow-up request driven by a server-provided `href` (challenge, verify, submit*, register, -/// update-password, poll). JSON encoded with a typed ``MSALNativeAuthV2RequestBody``. +/// A HAL follow-up request driven by a server-provided `href` struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { let context: MSALNativeAuthRequestContext let href: String diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift index 16bdda6238..d142903dac 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift @@ -83,7 +83,7 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { httpMethod: "POST", apiId: .telemetryApiIdV2ResetPasswordSubmitCode, operationType: MSALNativeAuthV2OperationType.verify.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", otp: "1234") + requestBody: MSALNativeAuthV2VerifyRequestBody(continuationToken: "CT", otp: "1234") ) XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmitCode) @@ -102,7 +102,7 @@ final class MSALNativeAuthV2ParametersTests: XCTestCase { httpMethod: "PUT", apiId: .telemetryApiIdV2ResetPasswordSubmit, operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, - requestBody: MSALNativeAuthV2RequestBody(continuationToken: "CT", newPassword: "newPass") + requestBody: MSALNativeAuthV2UpdatePasswordRequestBody(continuationToken: "CT", newPassword: "newPass") ) XCTAssertEqual(sut.httpMethod, "PUT") From 7f086187de8e03f6865ec6bfa04993b73903ad39 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Wed, 29 Jul 2026 21:47:34 +0100 Subject: [PATCH 82/85] Split MSALNativeAuthHALResponse by request --- MSAL/MSAL.xcodeproj/project.pbxproj | 36 ++++ ...tiveAuthHALAuthorizationCodeResponse.swift | 51 +++++ .../MSALNativeAuthHALChallengeResponse.swift | 67 ++++++ .../MSALNativeAuthHALCodeSentResponse.swift | 56 +++++ .../v2/MSALNativeAuthHALPollResponse.swift | 27 +++ ...NativeAuthHALReadyToCompleteResponse.swift | 27 +++ .../v2/MSALNativeAuthHALResponse.swift | 82 ++------ .../v2/MSALNativeAuthHALUpdateResponse.swift | 27 +++ ...SALNativeAuthV2HALResponseSerializer.swift | 191 ++++++++++++++---- .../MSALNativeAuthV2ResponseParser.swift | 96 ++++----- ...tiveAuthV2HALResponseSerializerTests.swift | 103 ++++++---- ...ativeAuthV2ResponseErrorHandlerTests.swift | 2 +- .../MSALNativeAuthV2ResponseParserTests.swift | 91 +++++++-- 13 files changed, 651 insertions(+), 205 deletions(-) create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALAuthorizationCodeResponse.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALCodeSentResponse.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALPollResponse.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALReadyToCompleteResponse.swift create mode 100644 MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALUpdateResponse.swift diff --git a/MSAL/MSAL.xcodeproj/project.pbxproj b/MSAL/MSAL.xcodeproj/project.pbxproj index 050d32303e..faba0c2980 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -451,6 +451,12 @@ 6A130FEA55D11486D2F4FA55 /* MSALNativeAuthCodeRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB0FFEFC459978DDDDE9212 /* MSALNativeAuthCodeRequiredState.swift */; }; 6B4459C145930D5EC63EA477 /* MSALNativeAuthMFAVerificationRequiredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439500470AC3BBD79E7D046 /* MSALNativeAuthMFAVerificationRequiredState.swift */; }; 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + 0F6295A1D71E8B874ADBC7AB /* MSALNativeAuthHALReadyToCompleteResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC43FA4B89AAF40D902A54 /* MSALNativeAuthHALReadyToCompleteResponse.swift */; }; + 64F5C9EE20EBA08795D0DB56 /* MSALNativeAuthHALAuthorizationCodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D992A9000DC4DCE7068F4E9 /* MSALNativeAuthHALAuthorizationCodeResponse.swift */; }; + 8152D13781903C8D0872832B /* MSALNativeAuthHALPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078016E3214C182662DA4C46 /* MSALNativeAuthHALPollResponse.swift */; }; + 6E254989A399FF946702D1AF /* MSALNativeAuthHALUpdateResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBAE8F887C06123BECF7F20 /* MSALNativeAuthHALUpdateResponse.swift */; }; + 3276108D1E5D204DF5582BB7 /* MSALNativeAuthHALCodeSentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F98BF51358D728874C29F1 /* MSALNativeAuthHALCodeSentResponse.swift */; }; + AD8F9352860B8BB8784A55A3 /* MSALNativeAuthHALChallengeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF0849BF9C6E8094353D212 /* MSALNativeAuthHALChallengeResponse.swift */; }; 6D9610BA7E33C6A269261772 /* MSALNativeAuthV2RequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5A5DE6C21C45276BE9736A /* MSALNativeAuthV2RequestBody.swift */; }; 584F147D25DE469499C2B538 /* MSALNativeAuthV2ChallengeRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02F30CED3374A7C81A93A4F /* MSALNativeAuthV2ChallengeRequestBody.swift */; }; 59B764D2DDE4420295E78F3C /* MSALNativeAuthV2PollRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729C79DAE4CA439EA155E210 /* MSALNativeAuthV2PollRequestBody.swift */; }; @@ -1061,6 +1067,12 @@ C5CCEC94B70DFFBB39C94BBF /* MSALNativeAuthFlowContinuationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */; }; C855BF96722554744C1E035E /* MSALNativeAuthV2EntryParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279AB45E50C40865E60DCD30 /* MSALNativeAuthV2EntryParameters.swift */; }; CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */; }; + 4076304A09CC32719F50ED6A /* MSALNativeAuthHALReadyToCompleteResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC43FA4B89AAF40D902A54 /* MSALNativeAuthHALReadyToCompleteResponse.swift */; }; + DF813C5ACBE0DC7BC2410819 /* MSALNativeAuthHALAuthorizationCodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D992A9000DC4DCE7068F4E9 /* MSALNativeAuthHALAuthorizationCodeResponse.swift */; }; + 10B85348A2C10AC18982316D /* MSALNativeAuthHALPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078016E3214C182662DA4C46 /* MSALNativeAuthHALPollResponse.swift */; }; + 419729F367DAFCB911C52995 /* MSALNativeAuthHALUpdateResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBAE8F887C06123BECF7F20 /* MSALNativeAuthHALUpdateResponse.swift */; }; + 268A97A2DFD4033E912F07E5 /* MSALNativeAuthHALCodeSentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3F98BF51358D728874C29F1 /* MSALNativeAuthHALCodeSentResponse.swift */; }; + A07004AB82351768F5BE2B15 /* MSALNativeAuthHALChallengeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF0849BF9C6E8094353D212 /* MSALNativeAuthHALChallengeResponse.swift */; }; CBD42DC826C8BC3C01077889 /* MSALNativeAuthFlowResponseDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */; }; D0BB51EAF53186287B322834 /* MSALNativeAuthV2RequestBodyKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89122AD33E47F9903341760A /* MSALNativeAuthV2RequestBodyKey.swift */; }; D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */; }; @@ -2428,6 +2440,12 @@ 963377BE211E14C600943EE0 /* MSALWebviewType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSALWebviewType.m; sourceTree = ""; }; 963C89A6214BA1760051AFEE /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALResponse.swift; sourceTree = ""; }; + 3AEC43FA4B89AAF40D902A54 /* MSALNativeAuthHALReadyToCompleteResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALReadyToCompleteResponse.swift; sourceTree = ""; }; + 0D992A9000DC4DCE7068F4E9 /* MSALNativeAuthHALAuthorizationCodeResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALAuthorizationCodeResponse.swift; sourceTree = ""; }; + 078016E3214C182662DA4C46 /* MSALNativeAuthHALPollResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALPollResponse.swift; sourceTree = ""; }; + 4EBAE8F887C06123BECF7F20 /* MSALNativeAuthHALUpdateResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALUpdateResponse.swift; sourceTree = ""; }; + F3F98BF51358D728874C29F1 /* MSALNativeAuthHALCodeSentResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALCodeSentResponse.swift; sourceTree = ""; }; + EFF0849BF9C6E8094353D212 /* MSALNativeAuthHALChallengeResponse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthHALChallengeResponse.swift; sourceTree = ""; }; 9648AF54225D826500F66801 /* MSALTelemetryConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALTelemetryConfig+Internal.h"; sourceTree = ""; }; 9648AF5B225DD6A900F66801 /* MSALGlobalConfig+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSALGlobalConfig+Internal.h"; sourceTree = ""; }; 9682A62A218290F700E37E63 /* MSALDefinitions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSALDefinitions.h; sourceTree = ""; }; @@ -3682,6 +3700,12 @@ isa = PBXGroup; children = ( 963C975607D3D8411DB7C13B /* MSALNativeAuthHALResponse.swift */, + 3AEC43FA4B89AAF40D902A54 /* MSALNativeAuthHALReadyToCompleteResponse.swift */, + 0D992A9000DC4DCE7068F4E9 /* MSALNativeAuthHALAuthorizationCodeResponse.swift */, + 078016E3214C182662DA4C46 /* MSALNativeAuthHALPollResponse.swift */, + 4EBAE8F887C06123BECF7F20 /* MSALNativeAuthHALUpdateResponse.swift */, + F3F98BF51358D728874C29F1 /* MSALNativeAuthHALCodeSentResponse.swift */, + EFF0849BF9C6E8094353D212 /* MSALNativeAuthHALChallengeResponse.swift */, 6B0FB1D8F96E014C18B59C51 /* MSALNativeAuthV2HALAction.swift */, FADE0000000000000000AA01 /* HALResource.swift */, C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */, @@ -7524,6 +7548,12 @@ 01F6FDA46510AF671264602E /* MSALNativeAuthFlowControlling.swift in Sources */, 8E0486CA55F25C1987E4067A /* MSALNativeAuthFlowController.swift in Sources */, CADE2AB39CE543C3F26FD20E /* MSALNativeAuthHALResponse.swift in Sources */, + 4076304A09CC32719F50ED6A /* MSALNativeAuthHALReadyToCompleteResponse.swift in Sources */, + DF813C5ACBE0DC7BC2410819 /* MSALNativeAuthHALAuthorizationCodeResponse.swift in Sources */, + 10B85348A2C10AC18982316D /* MSALNativeAuthHALPollResponse.swift in Sources */, + 419729F367DAFCB911C52995 /* MSALNativeAuthHALUpdateResponse.swift in Sources */, + 268A97A2DFD4033E912F07E5 /* MSALNativeAuthHALCodeSentResponse.swift in Sources */, + A07004AB82351768F5BE2B15 /* MSALNativeAuthHALChallengeResponse.swift in Sources */, 4B40B01DE4265B175930AC63 /* MSALNativeAuthV2HALAction.swift in Sources */, FADE0000000000000000AA02 /* HALResource.swift in Sources */, 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, @@ -7861,6 +7891,12 @@ E68C311BD4DDECABFAA212FD /* MSALNativeAuthFlowControlling.swift in Sources */, 76EEE63606562E71DCFDA606 /* MSALNativeAuthFlowController.swift in Sources */, 6C8FBDA4988619F52F72F92A /* MSALNativeAuthHALResponse.swift in Sources */, + 0F6295A1D71E8B874ADBC7AB /* MSALNativeAuthHALReadyToCompleteResponse.swift in Sources */, + 64F5C9EE20EBA08795D0DB56 /* MSALNativeAuthHALAuthorizationCodeResponse.swift in Sources */, + 8152D13781903C8D0872832B /* MSALNativeAuthHALPollResponse.swift in Sources */, + 6E254989A399FF946702D1AF /* MSALNativeAuthHALUpdateResponse.swift in Sources */, + 3276108D1E5D204DF5582BB7 /* MSALNativeAuthHALCodeSentResponse.swift in Sources */, + AD8F9352860B8BB8784A55A3 /* MSALNativeAuthHALChallengeResponse.swift in Sources */, 2161D7C3F3059052DD18D048 /* MSALNativeAuthV2HALAction.swift in Sources */, FADE0000000000000000AA03 /* HALResource.swift in Sources */, 665B1E32D6FFD77ED29F19FC /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */, diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALAuthorizationCodeResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALAuthorizationCodeResponse.swift new file mode 100644 index 0000000000..2ce2f66a89 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALAuthorizationCodeResponse.swift @@ -0,0 +1,51 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthHALAuthorizationCodeResponse: MSALNativeAuthHALResponse { + + /// Authorization code from the final `authorize-challenge` call. + let code: String + + init( + statusCode: Int, + correlationId: UUID?, + continuationToken: String?, + links: [String: String], + error: ServerError?, + isWebFallbackRequired: Bool, + code: String + ) { + self.code = code + super.init( + statusCode: statusCode, + correlationId: correlationId, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift new file mode 100644 index 0000000000..3fafbc5764 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). +struct EmbeddedMethod: Equatable { + let id: String? + let type: String? + let hint: String? + /// `_links` of the embedded method, keyed by relation (e.g. "challenge", "verify"), value is the raw href. + let links: [String: String] + + func link(for relation: MSALNativeAuthV2LinkRelation) -> String? { + return links[relation.rawValue] + } +} + +final class MSALNativeAuthHALChallengeResponse: MSALNativeAuthHALResponse { + + /// `_embedded.methods` entries. + let methods: [EmbeddedMethod] + let hint: String? + + init( + statusCode: Int, + correlationId: UUID?, + continuationToken: String?, + links: [String: String], + error: ServerError?, + isWebFallbackRequired: Bool, + methods: [EmbeddedMethod], + hint: String? + ) { + self.methods = methods + self.hint = hint + super.init( + statusCode: statusCode, + correlationId: correlationId, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALCodeSentResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALCodeSentResponse.swift new file mode 100644 index 0000000000..fef92fd374 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALCodeSentResponse.swift @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthHALCodeSentResponse: MSALNativeAuthHALResponse { + + let codeLength: Int? + let methodType: String? + let hint: String? + + init( + statusCode: Int, + correlationId: UUID?, + continuationToken: String?, + links: [String: String], + error: ServerError?, + isWebFallbackRequired: Bool, + codeLength: Int?, + methodType: String?, + hint: String? + ) { + self.codeLength = codeLength + self.methodType = methodType + self.hint = hint + super.init( + statusCode: statusCode, + correlationId: correlationId, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALPollResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALPollResponse.swift new file mode 100644 index 0000000000..345f7002d8 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALPollResponse.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthHALPollResponse: MSALNativeAuthHALResponse {} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALReadyToCompleteResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALReadyToCompleteResponse.swift new file mode 100644 index 0000000000..99cbddbe4e --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALReadyToCompleteResponse.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthHALReadyToCompleteResponse: MSALNativeAuthHALResponse {} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift index 1500b89b1c..58c1623ca3 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.swift @@ -24,35 +24,8 @@ @_implementationOnly import MSAL_Private -/// Generic, server-driven HAL response used by all Native Auth V2 flows. -/// -/// Every V2 HTTP outcome (200 success, the `401` from `authorize-challenge`, -/// and `4xx` error bodies) is parsed into a single ``MSALNativeAuthHALResponse``. The -/// V2 response validator then inspects `error`, `state` and `action` to decide how the -/// flow should proceed. HAL parsing itself is delegated to the shared -/// `HALResource` / `HALLink` Swift types. -struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { - - /// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). - struct EmbeddedMethod: Equatable { - let id: String? - let type: String? - let hint: String? - /// `_links` of the embedded method, keyed by relation (e.g. "challenge", "verify"), value is the raw href. - let links: [String: String] - - func link(for relation: MSALNativeAuthV2LinkRelation) -> String? { - return links[relation.rawValue] - } - } - - /// An attribute the server requires during sign up (`collectAttributes` action). - struct RequiredAttributeEntry: Equatable { - let id: String? - let type: String? - let required: Bool - let regex: String? - } +/// Base type for a server-driven HAL response used by the Native Auth V2 flows. +class MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { /// A server error body (`{ "error": { ... } }`). struct ServerError { @@ -65,47 +38,28 @@ struct MSALNativeAuthHALResponse: MSALNativeAuthResponseCorrelatable { let statusCode: Int var correlationId: UUID? - let state: String? - let action: String? let continuationToken: String? - let codeLength: Int? - let hint: String? - - /// The authentication factor required by a `challenge` action, taken from - /// `challengeContext.authenticationFactor` - let authenticationFactor: String? - /// Top-level method identifier (`id`) on method-style responses - let methodId: String? - /// Top-level method type (`type`, e.g. "email") on method-style responses. - let methodType: String? - /// Attributes the server requests on a sign up `collectAttributes` response. - let attributes: [RequiredAttributeEntry] - - /// Authorization code from the final `authorize-challenge` call. - let code: String? - - /// Top-level `_links`, keyed by relation, value is the raw href string. let links: [String: String] - /// `_embedded.methods` entries. - let methods: [EmbeddedMethod] let error: ServerError? - /// The typed `action` this response instructs the SDK to perform, or `nil` when absent/unknown. - var halAction: MSALNativeAuthV2HALAction? { - return action.flatMap(MSALNativeAuthV2HALAction.init(rawValue:)) - } - - var isReadyToComplete: Bool { - return state == "continue" - } - - /// `error == redirect_to_web` / `state == webFallbackRequired`: the server cannot continue this - /// flow natively and the app must fall back to interactive `acquireToken`. The `webFallback` link - /// is not used by the SDK. - var isWebFallbackRequired: Bool { - return error?.code == "redirect_to_web" || state == "webFallbackRequired" + let isWebFallbackRequired: Bool + + init( + statusCode: Int, + correlationId: UUID?, + continuationToken: String?, + links: [String: String], + error: ServerError?, + isWebFallbackRequired: Bool + ) { + self.statusCode = statusCode + self.correlationId = correlationId + self.continuationToken = continuationToken + self.links = links + self.error = error + self.isWebFallbackRequired = isWebFallbackRequired } func href(forRelation relation: String) -> String? { diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALUpdateResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALUpdateResponse.swift new file mode 100644 index 0000000000..5eb4d4dc38 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALUpdateResponse.swift @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +final class MSALNativeAuthHALUpdateResponse: MSALNativeAuthHALResponse {} diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index bf61ba4512..71acc5a8af 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -37,23 +37,13 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali let correlationId = MSALNativeAuthHALResponse.retrieveCorrelationIdFromHeaders(from: httpResponse) guard let data = data, !data.isEmpty else { - // an empty body is wrapped as an empty response, the validator rejects it as a general error. return MSALNativeAuthHALResponse( statusCode: statusCode, correlationId: correlationId, - state: nil, - action: nil, continuationToken: nil, - codeLength: nil, - hint: nil, - authenticationFactor: nil, - methodId: nil, - methodType: nil, - attributes: [], - code: nil, links: [:], - methods: [], - error: nil + error: nil, + isWebFallbackRequired: false ) } @@ -63,23 +53,156 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali } let resource = HALResource(json: json) - - return MSALNativeAuthHALResponse( + let state = resource.string(forKey: "state") + let error = parseError(from: json, fallbackCorrelationId: correlationId) + let base = BaseFields( statusCode: statusCode, correlationId: correlationId, - state: resource.string(forKey: "state"), - action: resource.string(forKey: "action"), continuationToken: resource.string(forKey: "continuationToken") ?? resource.string(forKey: "continuation_token"), - codeLength: json["codeLength"] as? Int, - hint: resource.string(forKey: "hint"), - authenticationFactor: (json["challengeContext"] as? [String: Any])?["authenticationFactor"] as? String, - methodId: resource.string(forKey: "id"), - methodType: resource.string(forKey: "type"), - attributes: parseAttributes(from: json), - code: resource.string(forKey: "code"), links: parseLinks(from: resource, json: json), - methods: parseMethods(from: resource), - error: parseError(from: json, fallbackCorrelationId: correlationId) + error: error, + isWebFallbackRequired: error?.code == "redirect_to_web" || state == "webFallbackRequired" + ) + + return makeConcreteResponse(resource: resource, json: json, state: state, base: base) + } + + /// Routes the parsed HAL body to the concrete response subclass that matches its shape. + private func makeConcreteResponse( + resource: HALResource, + json: [String: Any], + state: String?, + base: BaseFields + ) -> MSALNativeAuthHALResponse { + // The final authorize-challenge outcome carries an authorization code. + if let code = resource.string(forKey: "code") { + return makeAuthorizationCodeResponse(base, code: code) + } + + switch resource.string(forKey: "action").flatMap(MSALNativeAuthV2HALAction.init(rawValue:)) { + case .challenge: + return makeChallengeResponse(base, methods: parseMethods(from: resource), hint: resource.string(forKey: "hint")) + case .verify: + return makeCodeSentResponse( + base, + codeLength: json["codeLength"] as? Int, + methodType: resource.string(forKey: "type"), + hint: resource.string(forKey: "hint") + ) + case .update: + return makeUpdateResponse(base) + case .poll: + return makePollResponse(base) + default: + break + } + + if state == "continue" { + return makeReadyToCompleteResponse(base) + } + + return makeBaseResponse(base) + } + + /// The envelope fields shared by every concrete response, threaded through the factory helpers. + private struct BaseFields { + let statusCode: Int + let correlationId: UUID? + let continuationToken: String? + let links: [String: String] + let error: MSALNativeAuthHALResponse.ServerError? + let isWebFallbackRequired: Bool + } + + private func makeBaseResponse(_ base: BaseFields) -> MSALNativeAuthHALResponse { + return MSALNativeAuthHALResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired + ) + } + + private func makeChallengeResponse( + _ base: BaseFields, + methods: [MSALNativeAuthHALChallengeResponse.EmbeddedMethod], + hint: String? + ) -> MSALNativeAuthHALChallengeResponse { + return MSALNativeAuthHALChallengeResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired, + methods: methods, + hint: hint + ) + } + + private func makeCodeSentResponse( + _ base: BaseFields, + codeLength: Int?, + methodType: String?, + hint: String? + ) -> MSALNativeAuthHALCodeSentResponse { + return MSALNativeAuthHALCodeSentResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired, + codeLength: codeLength, + methodType: methodType, + hint: hint + ) + } + + private func makeUpdateResponse(_ base: BaseFields) -> MSALNativeAuthHALUpdateResponse { + return MSALNativeAuthHALUpdateResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired + ) + } + + private func makePollResponse(_ base: BaseFields) -> MSALNativeAuthHALPollResponse { + return MSALNativeAuthHALPollResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired + ) + } + + private func makeReadyToCompleteResponse(_ base: BaseFields) -> MSALNativeAuthHALReadyToCompleteResponse { + return MSALNativeAuthHALReadyToCompleteResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired + ) + } + + private func makeAuthorizationCodeResponse(_ base: BaseFields, code: String) -> MSALNativeAuthHALAuthorizationCodeResponse { + return MSALNativeAuthHALAuthorizationCodeResponse( + statusCode: base.statusCode, + correlationId: base.correlationId, + continuationToken: base.continuationToken, + links: base.links, + error: base.error, + isWebFallbackRequired: base.isWebFallbackRequired, + code: code ) } @@ -99,7 +222,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali return result } - private func parseMethods(from resource: HALResource) -> [MSALNativeAuthHALResponse.EmbeddedMethod] { + private func parseMethods(from resource: HALResource) -> [MSALNativeAuthHALChallengeResponse.EmbeddedMethod] { let methodResources = resource.embeddedResources(rel: "methods") return methodResources.map { dict in let methodResource = HALResource(json: dict) @@ -109,7 +232,7 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali links[relation] = href } } - return MSALNativeAuthHALResponse.EmbeddedMethod( + return MSALNativeAuthHALChallengeResponse.EmbeddedMethod( id: methodResource.string(forKey: "id"), type: methodResource.string(forKey: "type"), hint: methodResource.string(forKey: "hint"), @@ -118,20 +241,6 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali } } - private func parseAttributes(from json: [String: Any]) -> [MSALNativeAuthHALResponse.RequiredAttributeEntry] { - guard let rawAttributes = json["attributes"] as? [[String: Any]] else { - return [] - } - return rawAttributes.map { dict in - MSALNativeAuthHALResponse.RequiredAttributeEntry( - id: (dict["attributeId"] as? String) ?? (dict["id"] as? String), - type: dict["type"] as? String, - required: (dict["required"] as? Bool) ?? false, - regex: (dict["validationRegex"] as? String) ?? (dict["regex"] as? String) - ) - } - } - private func parseError(from json: [String: Any], fallbackCorrelationId: UUID?) -> MSALNativeAuthHALResponse.ServerError? { guard let errorDict = json["error"] as? [String: Any] else { return nil diff --git a/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift index 62c40cbcac..dfff9a7bee 100644 --- a/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift +++ b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift @@ -51,7 +51,7 @@ final class MSALNativeAuthV2ResponseParser: MSALNativeAuthV2ResponseParsing { if let error = response.error { return .error(flowError(from: error, context: context)) } - if let code = response.code { + if let code = (response as? MSALNativeAuthHALAuthorizationCodeResponse)?.code { MSALNativeAuthLogger.log(level: .verbose, context: context, format: "authorize-challenge: received authorization code") return .authorizationCode(code: code) } @@ -92,7 +92,7 @@ final class MSALNativeAuthV2ResponseParser: MSALNativeAuthV2ResponseParsing { return .error(flowError(from: error, context: context)) } - if response.isReadyToComplete { + if response is MSALNativeAuthHALReadyToCompleteResponse { guard let continuationToken = response.continuationToken else { MSALNativeAuthLogger.log( level: .error, @@ -109,51 +109,57 @@ final class MSALNativeAuthV2ResponseParser: MSALNativeAuthV2ResponseParsing { return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing continuation token in interaction response")) } - MSALNativeAuthLogger.log(level: .verbose, context: context, format: "interaction: processing action '%@'", response.action ?? "nil") + return parseInteractionResponse(response, continuationToken: continuationToken, context: context) + } + } - switch response.halAction { - case .challenge: - let method = response.methods.first - guard let challengeHref = method?.link(for: .challenge) ?? response.href(for: .challenge) else { - return missingLink(.challenge, context: context) - } - return .challengeRequired( - continuationToken: continuationToken, - challengeHref: challengeHref, - hint: method?.hint ?? response.hint - ) - case .verify: - guard let verifyHref = response.href(for: .verify) else { - return missingLink(.verify, context: context) - } - return .codeRequired( - continuationToken: continuationToken, - verifyHref: verifyHref, - resendHref: response.href(for: .resend), - sentTo: response.hint ?? "", - channelType: MSALNativeAuthChannelType(value: response.methodType ?? "email"), - codeLength: response.codeLength ?? 0 - ) - case .update: - guard let updateHref = response.href(for: .update) ?? response.href(for: .self) else { - return missingLink(.update, context: context) - } - return .updateRequired( - continuationToken: continuationToken, - updateHref: updateHref - ) - case .poll: - guard let pollHref = response.href(for: .poll) else { - return missingLink(.poll, context: context) - } - return .pollInProgress( - continuationToken: continuationToken, - pollHref: pollHref - ) - default: - MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: unexpected action '%@'", response.action ?? "nil") - return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected action '\(response.action ?? "nil")'")) + private func parseInteractionResponse( + _ response: MSALNativeAuthHALResponse, + continuationToken: String, + context: MSIDRequestContext + ) -> MSALNativeAuthV2InteractionParsedResponse { + switch response { + case let challengeResponse as MSALNativeAuthHALChallengeResponse: + let method = challengeResponse.methods.first + guard let challengeHref = method?.link(for: .challenge) ?? challengeResponse.href(for: .challenge) else { + return missingLink(.challenge, context: context) + } + return .challengeRequired( + continuationToken: continuationToken, + challengeHref: challengeHref, + hint: method?.hint ?? challengeResponse.hint + ) + case let codeSentResponse as MSALNativeAuthHALCodeSentResponse: + guard let verifyHref = codeSentResponse.href(for: .verify) else { + return missingLink(.verify, context: context) + } + return .codeRequired( + continuationToken: continuationToken, + verifyHref: verifyHref, + resendHref: codeSentResponse.href(for: .resend), + sentTo: codeSentResponse.hint ?? "", + channelType: MSALNativeAuthChannelType(value: codeSentResponse.methodType ?? "email"), + codeLength: codeSentResponse.codeLength ?? 0 + ) + case let updateResponse as MSALNativeAuthHALUpdateResponse: + guard let updateHref = updateResponse.href(for: .update) ?? updateResponse.href(for: .self) else { + return missingLink(.update, context: context) + } + return .updateRequired( + continuationToken: continuationToken, + updateHref: updateHref + ) + case let pollResponse as MSALNativeAuthHALPollResponse: + guard let pollHref = pollResponse.href(for: .poll) else { + return missingLink(.poll, context: context) } + return .pollInProgress( + continuationToken: continuationToken, + pollHref: pollHref + ) + default: + MSALNativeAuthLogger.log(level: .error, context: context, format: "interaction: unexpected response type") + return .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected interaction response")) } } } diff --git a/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift index 5983fb4460..35b3f2ccdf 100644 --- a/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift +++ b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift @@ -30,33 +30,71 @@ final class MSALNativeAuthV2HALResponseSerializerTests: XCTestCase { private let sut = MSALNativeAuthV2HALResponseSerializer() - // MARK: - Top-level scalar fields + // MARK: - Concrete response types - func test_responseObject_parsesTopLevelScalarFields() throws { + func test_responseObject_challengeAction_returnsChallengeResponse() throws { let json: [String: Any] = [ "state": "interactionRequired", "action": "challenge", + "continuationToken": "ct-123", + "hint": "u***@contoso.com" + ] + + let response = try parse(json, statusCode: 200) + let challenge = try XCTUnwrap(response as? MSALNativeAuthHALChallengeResponse) + + XCTAssertEqual(challenge.statusCode, 200) + XCTAssertEqual(challenge.continuationToken, "ct-123") + XCTAssertEqual(challenge.hint, "u***@contoso.com") + } + + func test_responseObject_verifyAction_returnsCodeSentResponse() throws { + let json: [String: Any] = [ + "state": "interactionRequired", + "action": "verify", "continuation_token": "ct-123", "codeLength": 8, "hint": "u***@contoso.com", - "id": "method-1", - "type": "email", - "code": "auth-code", - "challengeContext": ["authenticationFactor": "oob"] + "type": "email" ] let response = try parse(json, statusCode: 200) + let codeSent = try XCTUnwrap(response as? MSALNativeAuthHALCodeSentResponse) - XCTAssertEqual(response.statusCode, 200) - XCTAssertEqual(response.state, "interactionRequired") - XCTAssertEqual(response.action, "challenge") - XCTAssertEqual(response.continuationToken, "ct-123") - XCTAssertEqual(response.codeLength, 8) - XCTAssertEqual(response.hint, "u***@contoso.com") - XCTAssertEqual(response.methodId, "method-1") - XCTAssertEqual(response.methodType, "email") - XCTAssertEqual(response.code, "auth-code") - XCTAssertEqual(response.authenticationFactor, "oob") + XCTAssertEqual(codeSent.continuationToken, "ct-123") + XCTAssertEqual(codeSent.codeLength, 8) + XCTAssertEqual(codeSent.hint, "u***@contoso.com") + XCTAssertEqual(codeSent.methodType, "email") + } + + func test_responseObject_updateAction_returnsUpdateResponse() throws { + let json: [String: Any] = ["state": "interactionRequired", "action": "update", "continuationToken": "ct"] + let response = try parse(json, statusCode: 200) + XCTAssertTrue(response is MSALNativeAuthHALUpdateResponse) + } + + func test_responseObject_pollAction_returnsPollResponse() throws { + let json: [String: Any] = ["state": "interactionRequired", "action": "poll", "continuationToken": "ct"] + let response = try parse(json, statusCode: 200) + XCTAssertTrue(response is MSALNativeAuthHALPollResponse) + } + + func test_responseObject_continueState_returnsReadyToCompleteResponse() throws { + let json: [String: Any] = ["state": "continue", "continuationToken": "ct"] + let response = try parse(json, statusCode: 200) + XCTAssertTrue(response is MSALNativeAuthHALReadyToCompleteResponse) + XCTAssertEqual(response.continuationToken, "ct") + } + + func test_responseObject_code_returnsAuthorizationCodeResponse() throws { + let response = try parse(["code": "auth-code"], statusCode: 200) + let codeResponse = try XCTUnwrap(response as? MSALNativeAuthHALAuthorizationCodeResponse) + XCTAssertEqual(codeResponse.code, "auth-code") + } + + func test_responseObject_webFallbackState_setsIsWebFallbackRequired() throws { + let response = try parse(["state": "webFallbackRequired", "continuationToken": "ct"], statusCode: 200) + XCTAssertTrue(response.isWebFallbackRequired) } func test_responseObject_prefersCamelCaseContinuationToken() throws { @@ -89,6 +127,7 @@ final class MSALNativeAuthV2HALResponseSerializerTests: XCTestCase { func test_responseObject_parsesEmbeddedMethods() throws { let json: [String: Any] = [ + "action": "challenge", "_embedded": [ "methods": [ [ @@ -102,32 +141,16 @@ final class MSALNativeAuthV2HALResponseSerializerTests: XCTestCase { ] let response = try parse(json, statusCode: 200) + let challenge = try XCTUnwrap(response as? MSALNativeAuthHALChallengeResponse) - XCTAssertEqual(response.methods.count, 1) - let method = try XCTUnwrap(response.methods.first) + XCTAssertEqual(challenge.methods.count, 1) + let method = try XCTUnwrap(challenge.methods.first) XCTAssertEqual(method.id, "1") XCTAssertEqual(method.type, "email") XCTAssertEqual(method.hint, "u***@contoso.com") XCTAssertEqual(method.link(for: .challenge), "https://contoso.com/challenge") } - // MARK: - Attributes - - func test_responseObject_parsesAttributes() throws { - let json: [String: Any] = [ - "attributes": [ - ["attributeId": "email", "type": "string", "required": true, "validationRegex": ".+@.+"], - ["id": "displayName", "type": "string"] - ] - ] - - let response = try parse(json, statusCode: 200) - - XCTAssertEqual(response.attributes.count, 2) - XCTAssertEqual(response.attributes[0], .init(id: "email", type: "string", required: true, regex: ".+@.+")) - XCTAssertEqual(response.attributes[1], .init(id: "displayName", type: "string", required: false, regex: nil)) - } - // MARK: - Server error func test_responseObject_parsesServerError() throws { @@ -156,6 +179,11 @@ final class MSALNativeAuthV2HALResponseSerializerTests: XCTestCase { XCTAssertEqual(response.error?.correlationId, correlationId) } + func test_responseObject_redirectToWebError_setsIsWebFallbackRequired() throws { + let response = try parse(["error": ["code": "redirect_to_web"]], statusCode: 400) + XCTAssertTrue(response.isWebFallbackRequired) + } + // MARK: - Empty / malformed bodies func test_responseObject_emptyData_returnsEmptyResponseWithStatusCode() throws { @@ -164,11 +192,10 @@ final class MSALNativeAuthV2HALResponseSerializerTests: XCTestCase { let response = try XCTUnwrap(result as? MSALNativeAuthHALResponse) XCTAssertEqual(response.statusCode, 204) - XCTAssertNil(response.state) - XCTAssertNil(response.action) + XCTAssertNil(response.continuationToken) XCTAssertNil(response.error) XCTAssertTrue(response.links.isEmpty) - XCTAssertTrue(response.methods.isEmpty) + XCTAssertFalse(response.isWebFallbackRequired) } func test_responseObject_nilHTTPResponse_defaultsStatusCodeToZero() throws { diff --git a/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift index 9e38b80971..3c7d33e02d 100644 --- a/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift +++ b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandlerTests.swift @@ -82,7 +82,7 @@ final class MSALNativeAuthV2ResponseErrorHandlerTests: XCTestCase { } wait(for: [expectation], timeout: 1) - XCTAssertEqual(receivedResponse?.state, "continue") + XCTAssertTrue(receivedResponse is MSALNativeAuthHALReadyToCompleteResponse) } func test_handleError_nonJSONBody_returnsError() { diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift index 85b7fad12f..84d423d296 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift @@ -46,31 +46,90 @@ final class MSALNativeAuthV2ResponseParserTests: XCTestCase { continuationToken: String? = nil, codeLength: Int? = nil, hint: String? = nil, - authenticationFactor: String? = nil, - methodId: String? = nil, methodType: String? = nil, - attributes: [MSALNativeAuthHALResponse.RequiredAttributeEntry] = [], code: String? = nil, links: [String: String] = [:], - methods: [MSALNativeAuthHALResponse.EmbeddedMethod] = [], + methods: [MSALNativeAuthHALChallengeResponse.EmbeddedMethod] = [], error: MSALNativeAuthHALResponse.ServerError? = nil ) -> MSALNativeAuthHALResponse { + let isWebFallbackRequired = error?.code == "redirect_to_web" || state == "webFallbackRequired" + + if let code = code { + return MSALNativeAuthHALAuthorizationCodeResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired, + code: code + ) + } + + switch action.flatMap(MSALNativeAuthV2HALAction.init(rawValue:)) { + case .challenge: + return MSALNativeAuthHALChallengeResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired, + methods: methods, + hint: hint + ) + case .verify: + return MSALNativeAuthHALCodeSentResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired, + codeLength: codeLength, + methodType: methodType, + hint: hint + ) + case .update: + return MSALNativeAuthHALUpdateResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + case .poll: + return MSALNativeAuthHALPollResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + default: + break + } + + if state == "continue" { + return MSALNativeAuthHALReadyToCompleteResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + } + return MSALNativeAuthHALResponse( statusCode: statusCode, correlationId: nil, - state: state, - action: action, continuationToken: continuationToken, - codeLength: codeLength, - hint: hint, - authenticationFactor: authenticationFactor, - methodId: methodId, - methodType: methodType, - attributes: attributes, - code: code, links: links, - methods: methods, - error: error + error: error, + isWebFallbackRequired: isWebFallbackRequired ) } @@ -114,7 +173,7 @@ final class MSALNativeAuthV2ResponseParserTests: XCTestCase { // MARK: - parseInteraction func test_parseInteraction_challengeAction_returnsChallengeRequired() { - let method = MSALNativeAuthHALResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) + let method = MSALNativeAuthHALChallengeResponse.EmbeddedMethod(id: "1", type: "email", hint: "u***@contoso.com", links: ["challenge": "https://contoso.com/challenge"]) let response = makeResponse(state: "interactionRequired", action: "challenge", continuationToken: "ct", methods: [method]) let result = sut.parseInteraction(context: context, .success(response)) XCTAssertEqual(result, .challengeRequired(continuationToken: "ct", challengeHref: "https://contoso.com/challenge", hint: "u***@contoso.com")) From 5cce4cf51112b3d08aabb428cc49a9295d1e38e3 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Thu, 30 Jul 2026 16:51:13 +0100 Subject: [PATCH 83/85] Fix build issues --- .../MSALNativeAuthHALChallengeResponse.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift index 3fafbc5764..5458ddef88 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALChallengeResponse.swift @@ -24,20 +24,20 @@ import Foundation -/// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). -struct EmbeddedMethod: Equatable { - let id: String? - let type: String? - let hint: String? - /// `_links` of the embedded method, keyed by relation (e.g. "challenge", "verify"), value is the raw href. - let links: [String: String] +final class MSALNativeAuthHALChallengeResponse: MSALNativeAuthHALResponse { - func link(for relation: MSALNativeAuthV2LinkRelation) -> String? { - return links[relation.rawValue] - } -} + /// A method embedded in a HAL `_embedded.methods` array (e.g. an email OTP method). + struct EmbeddedMethod: Equatable { + let id: String? + let type: String? + let hint: String? + /// `_links` of the embedded method, keyed by relation (e.g. "challenge", "verify"), value is the raw href. + let links: [String: String] -final class MSALNativeAuthHALChallengeResponse: MSALNativeAuthHALResponse { + func link(for relation: MSALNativeAuthV2LinkRelation) -> String? { + return links[relation.rawValue] + } + } /// `_embedded.methods` entries. let methods: [EmbeddedMethod] From caecbd4b72107de7ad0f23109fb2131eaa261ca8 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 31 Jul 2026 14:36:31 +0100 Subject: [PATCH 84/85] Update HalAction --- .../v2/MSALNativeAuthV2HALAction.swift | 45 +++++++--- ...SALNativeAuthV2HALResponseSerializer.swift | 34 +++---- .../MSALNativeAuthV2ResponseParserTests.swift | 90 ++++++++++--------- 3 files changed, 99 insertions(+), 70 deletions(-) diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift index e11288bbe5..01224eaada 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift @@ -27,14 +27,39 @@ import Foundation /// The `action` a Native Auth V2 (HAL) interaction response instructs the SDK to perform next. /// /// The validator maps the raw `action` string carried by ``MSALNativeAuthHALResponse`` onto one of -/// these cases to decide the next step of the flow. -enum MSALNativeAuthV2HALAction: String { - case challenge - case verify - case enroll - case register - case activate - case collectAttributes - case update - case poll +/// these values to decide the next step of the flow. +struct MSALNativeAuthV2HALAction: RawRepresentable, Hashable { + let rawValue: String +} + +extension MSALNativeAuthV2HALAction { + static let challenge = Self(rawValue: "challenge") +} + +extension MSALNativeAuthV2HALAction { + static let verify = Self(rawValue: "verify") +} + +extension MSALNativeAuthV2HALAction { + static let enroll = Self(rawValue: "enroll") +} + +extension MSALNativeAuthV2HALAction { + static let register = Self(rawValue: "register") +} + +extension MSALNativeAuthV2HALAction { + static let activate = Self(rawValue: "activate") +} + +extension MSALNativeAuthV2HALAction { + static let collectAttributes = Self(rawValue: "collectAttributes") +} + +extension MSALNativeAuthV2HALAction { + static let update = Self(rawValue: "update") +} + +extension MSALNativeAuthV2HALAction { + static let poll = Self(rawValue: "poll") } diff --git a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift index 71acc5a8af..bcae7ca811 100644 --- a/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -79,22 +79,24 @@ final class MSALNativeAuthV2HALResponseSerializer: NSObject, MSIDResponseSeriali return makeAuthorizationCodeResponse(base, code: code) } - switch resource.string(forKey: "action").flatMap(MSALNativeAuthV2HALAction.init(rawValue:)) { - case .challenge: - return makeChallengeResponse(base, methods: parseMethods(from: resource), hint: resource.string(forKey: "hint")) - case .verify: - return makeCodeSentResponse( - base, - codeLength: json["codeLength"] as? Int, - methodType: resource.string(forKey: "type"), - hint: resource.string(forKey: "hint") - ) - case .update: - return makeUpdateResponse(base) - case .poll: - return makePollResponse(base) - default: - break + if let actionValue = resource.string(forKey: "action") { + switch MSALNativeAuthV2HALAction(rawValue: actionValue) { + case .challenge: + return makeChallengeResponse(base, methods: parseMethods(from: resource), hint: resource.string(forKey: "hint")) + case .verify: + return makeCodeSentResponse( + base, + codeLength: json["codeLength"] as? Int, + methodType: resource.string(forKey: "type"), + hint: resource.string(forKey: "hint") + ) + case .update: + return makeUpdateResponse(base) + case .poll: + return makePollResponse(base) + default: + break + } } if state == "continue" { diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift index 84d423d296..7bd203a801 100644 --- a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift @@ -66,50 +66,52 @@ final class MSALNativeAuthV2ResponseParserTests: XCTestCase { ) } - switch action.flatMap(MSALNativeAuthV2HALAction.init(rawValue:)) { - case .challenge: - return MSALNativeAuthHALChallengeResponse( - statusCode: statusCode, - correlationId: nil, - continuationToken: continuationToken, - links: links, - error: error, - isWebFallbackRequired: isWebFallbackRequired, - methods: methods, - hint: hint - ) - case .verify: - return MSALNativeAuthHALCodeSentResponse( - statusCode: statusCode, - correlationId: nil, - continuationToken: continuationToken, - links: links, - error: error, - isWebFallbackRequired: isWebFallbackRequired, - codeLength: codeLength, - methodType: methodType, - hint: hint - ) - case .update: - return MSALNativeAuthHALUpdateResponse( - statusCode: statusCode, - correlationId: nil, - continuationToken: continuationToken, - links: links, - error: error, - isWebFallbackRequired: isWebFallbackRequired - ) - case .poll: - return MSALNativeAuthHALPollResponse( - statusCode: statusCode, - correlationId: nil, - continuationToken: continuationToken, - links: links, - error: error, - isWebFallbackRequired: isWebFallbackRequired - ) - default: - break + if let action = action { + switch MSALNativeAuthV2HALAction(rawValue: action) { + case .challenge: + return MSALNativeAuthHALChallengeResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired, + methods: methods, + hint: hint + ) + case .verify: + return MSALNativeAuthHALCodeSentResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired, + codeLength: codeLength, + methodType: methodType, + hint: hint + ) + case .update: + return MSALNativeAuthHALUpdateResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + case .poll: + return MSALNativeAuthHALPollResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + default: + break + } } if state == "continue" { From c6f3d1b2262bcc54347027e4cbcf73b0c9a913d5 Mon Sep 17 00:00:00 2001 From: Silviu Petrescu Date: Fri, 31 Jul 2026 15:15:07 +0100 Subject: [PATCH 85/85] First check for error before guard poll --- .../v2/MSALNativeAuthFlowController.swift | 26 +++++++++- .../MSALNativeAuthFlowControllerTests.swift | 51 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift index 9af1bdeac5..88f60f4fce 100644 --- a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -202,8 +202,24 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati ) } + if case .error(let error) = updateResult { + return interactionFailure( + updateResult, + event: event, + context: context, + scenario: flowContinuationState.flowScenario, + newState: error.type == .invalidPassword ? state : nil + ) + } + guard case .pollInProgress(var pollToken, let pollHref) = updateResult else { - return interactionFailure(updateResult, event: event, context: context, scenario: flowContinuationState.flowScenario, newState: nil) + return interactionFailure( + updateResult, + event: event, + context: context, + scenario: flowContinuationState.flowScenario, + newState: nil + ) } let retryExecutor = MSALNativeAuthRetryExecutor(delays: [pollIntervalSeconds]) @@ -241,7 +257,13 @@ final class MSALNativeAuthFlowController: MSALNativeAuthBaseController, MSALNati } guard case .readyToComplete(let completionToken) = terminalPollResult else { - return interactionFailure(terminalPollResult, event: event, context: context, scenario: flowContinuationState.flowScenario, newState: nil) + return interactionFailure( + terminalPollResult, + event: event, + context: context, + scenario: flowContinuationState.flowScenario, + newState: nil + ) } let step = MSALNativeAuthFlowStepContext(apiId: .telemetryApiIdV2ResetPasswordSubmit, event: event, context: context) diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift index 852bfd718f..e1a89a1f45 100644 --- a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -215,6 +215,57 @@ final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { XCTAssertFalse(requestProviderMock.updatePasswordCalled) } + func test_submitNewPassword_whenUpdateRejectsWeakPassword_isRecoverable() async { + requestProviderMock.mockRequest() + parserMock.interactionResponses = [ + .error(MSALNativeAuthFlowError(type: .invalidPassword)) + ] + let state = makeState(links: [.update: URL(string: "https://contoso.com/update")!]) + + let response = await sut.submitNewPassword("weak", state: state) + + guard case .error(let error, let newState) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertEqual(error.type, .invalidPassword) + XCTAssertNotNil(newState) + XCTAssertTrue(requestProviderMock.updatePasswordCalled) + XCTAssertFalse(requestProviderMock.pollCalled) + } + + func test_submitNewPassword_whenPollReturnsError_isNotRecoverable() async { + requestProviderMock.mockRequest() + parserMock.interactionResponses = [ + .pollInProgress(continuationToken: "ct-poll", pollHref: "https://contoso.com/poll"), + .error(MSALNativeAuthFlowError(type: .invalidPassword)) + ] + let state = makeState(links: [.update: URL(string: "https://contoso.com/update")!]) + + let response = await sut.submitNewPassword("New-Password-1", state: state) + + guard case .error(_, let newState) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertNil(newState) + XCTAssertTrue(requestProviderMock.pollCalled) + XCTAssertFalse(requestProviderMock.tokenCalled) + } + + func test_submitNewPassword_whenUpdateReturnsGeneralError_isNotRecoverable() async { + requestProviderMock.mockRequest() + parserMock.interactionResponses = [ + .error(MSALNativeAuthFlowError(type: .generalError)) + ] + let state = makeState(links: [.update: URL(string: "https://contoso.com/update")!]) + + let response = await sut.submitNewPassword("New-Password-1", state: state) + + guard case .error(_, let newState) = response.result else { + return XCTFail("Expected error, got \(response.result)") + } + XCTAssertNil(newState) + } + // MARK: - resendCode func test_resendCode_whenCodeRequired_returnsCodeRequired() async {