diff --git a/.clinerules/04-Code-style-guidelines.md b/.clinerules/04-Code-style-guidelines.md index 95801120f8..532b57a65b 100644 --- a/.clinerules/04-Code-style-guidelines.md +++ b/.clinerules/04-Code-style-guidelines.md @@ -555,3 +555,84 @@ 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`) **MUST** follow the SwiftLint rules from `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. + +```swift +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. +- 4-space indentation, never tabs. + +### function_body_length & cyclomatic_complexity — prefer suppression over refactor + +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`: + + ```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..364e72d24a 100644 --- a/.clinerules/AGENTS.md +++ b/.clinerules/AGENTS.md @@ -26,6 +26,14 @@ 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. + +## 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` 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 9d3dfce677..faba0c2980 100644 --- a/MSAL/MSAL.xcodeproj/project.pbxproj +++ b/MSAL/MSAL.xcodeproj/project.pbxproj @@ -8,32 +8,10 @@ /* Begin PBXBuildFile section */ 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 */; }; @@ -107,7 +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 */; }; - 0B808ECA169C3107F4335691 /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; + 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 */; }; 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 */; }; @@ -115,8 +95,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 */; }; + 192F74D7E3825C5CDCF50CEB /* MSALNativeAuthV2HrefURLResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.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 */; }; @@ -175,6 +158,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 */; }; @@ -263,6 +248,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 */; }; @@ -416,15 +402,41 @@ 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 */; }; - 31A0E8B0B69F886271896E3E /* RetryExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AAD919E560052DA700D2DA /* RetryExecutor.swift */; }; + 2A771DF95BAF81DFD3AA525E /* MSALNativeAuthV2HALResponseSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0168D434625F66BAEAA2ED1 /* MSALNativeAuthV2HALResponseSerializer.swift */; }; + 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; @@ -432,7 +444,25 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; 7207E63A2FA58EA3008F6803 /* MSALDeviceTokenResult+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7207E6382FA58E8F008F6803 /* MSALDeviceTokenResult+Internal.h */; }; @@ -442,6 +472,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 /* 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, ); }; }; @@ -457,6 +488,20 @@ 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 */; }; + 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 */; }; @@ -471,6 +516,9 @@ 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 /* 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 */; }; @@ -481,9 +529,11 @@ 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 */; }; + 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 */; }; @@ -501,6 +551,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 /* 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 */; }; @@ -508,6 +559,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 */; }; @@ -556,6 +608,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 */; }; @@ -583,6 +636,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 /* 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 */; }; @@ -593,6 +647,12 @@ 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 /* MSALNativeAuthFlowControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */; }; + A4B46554DA558BD5457129CF /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57C7FF4AC0AEE2CF96F84C7 /* MSALNativeAuthV2AuthorizeChallengeContinueParameters.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 */; }; 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 */; }; @@ -630,6 +690,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 */; }; @@ -992,6 +1053,33 @@ 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 /* 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; @@ -1023,6 +1111,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 */; }; @@ -1128,18 +1217,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 */; }; @@ -1163,6 +1248,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 */; }; @@ -1579,6 +1665,9 @@ 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 */; }; E2025D202B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2025D1F2B2B8EEA00E32871 /* MSALNativeAuthSubErrorCodeTests.swift */; }; E205D62E29B783FF003887BC /* MSALNativeAuthInternalConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E205D62D29B783FF003887BC /* MSALNativeAuthInternalConfiguration.swift */; }; @@ -1707,6 +1796,19 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */ @@ -2060,9 +2162,19 @@ 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 = ""; }; + 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 = ""; }; 1E1A2E052256D194001009ED /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -2147,6 +2259,8 @@ 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 = ""; }; 28188F5F2C8F482D00CFDD05 /* MSALNativeAuthSignInWithMFAEndToEndTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSALNativeAuthSignInWithMFAEndToEndTests.swift; sourceTree = ""; }; @@ -2244,29 +2358,27 @@ 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 = ""; }; + 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 = ""; }; - 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 = ""; }; 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 = ""; }; 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 /* 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 = ""; }; @@ -2274,14 +2386,24 @@ 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 /* 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 = ""; }; + 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 = ""; }; @@ -2293,6 +2415,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 = ""; }; @@ -2316,6 +2439,13 @@ 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 = ""; }; + 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 = ""; }; @@ -2341,6 +2471,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 = ""; }; @@ -2352,6 +2483,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 = ""; }; @@ -2369,9 +2501,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 /* 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -2527,7 +2662,20 @@ 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 /* 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 /* 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -2653,12 +2801,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 = ""; }; @@ -2912,6 +3058,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 */ @@ -3103,6 +3256,51 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 03D8CB5A8588FA78B92F2A2D /* v2 */ = { + isa = PBXGroup; + children = ( + 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 */, + 5731594DCD4698A57CAB2D10 /* parameters */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 1718011BA0E3C2154663D13F /* v2 */ = { + isa = PBXGroup; + children = ( + 7E10D43EA340CAA107F73114 /* MSALNativeAuthFlowResult.swift */, + 49AAD919E560052DA700D2DA /* MSALNativeAuthRetryExecutor.swift */, + B5A05A2777A8FD6E91679121 /* MSALNativeAuthFlowContinuationState.swift */, + 547DFB0174DCC5EAB169C72A /* MSALNativeAuthFlowControllerResponse.swift */, + 8AFD2116FBA7674AD0CE9E36 /* MSALNativeAuthFlowResponseDispatcher.swift */, + 6219CABBCE9D363C142DCC96 /* MSALNativeAuthFlowInternalState.swift */, + B414350D2B1EE1FA349DC550 /* MSALNativeAuthFlowControlling.swift */, + C3B8230A5B6672389A1A6075 /* MSALNativeAuthFlowController.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 1A153D161101EAF09A32E906 /* parser */ = { + isa = PBXGroup; + children = ( + 1D04D5E6EC9281BEF684A520 /* MSALNativeAuthV2ParsedResponses.swift */, + 6E0642C9775DBBD741FDA753 /* MSALNativeAuthV2ResponseParser.swift */, + ); + name = parser; + path = parser; + sourceTree = ""; + }; 2811CDCF296F16DE007BA21B /* controllers */ = { isa = PBXGroup; children = ( @@ -3116,6 +3314,8 @@ E2C1D286299BA15D00B26449 /* MSALNativeAuthBaseController.swift */, DE54B5902A434B9B00460B34 /* MSALNativeAuthTokenController.swift */, E2EFAD152A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift */, + 1718011BA0E3C2154663D13F /* v2 */, + 3987E863BBAFB83CC165547E /* MSALNativeAuthTokenRequestHandling.swift */, ); path = controllers; sourceTree = ""; @@ -3208,6 +3408,7 @@ DEFE87682CA6BC3A009D11DC /* MSALNativeAuthSilentTokenProviderFactoryMock.swift */, DEFE87692CA6BC3A009D11DC /* MSALNativeAuthSilentTokenProviderMock.swift */, 9B61C91D2A27E5E200CE9E3A /* reset_password */, + 9279009AB4669F3B2A4D66FD /* v2 */, ); path = mock; sourceTree = ""; @@ -3238,7 +3439,6 @@ 28A277D82C22ED5E00D95E00 /* MSALNativeAuthEmailCodeRetriever.swift */, C8B4CF9C872C00B3E5FD2C40 /* MailTMConstants.swift */, 475F1413DA1D76D5EF31F4EC /* MailTMHTTPClient.swift */, - 49AAD919E560052DA700D2DA /* RetryExecutor.swift */, ); path = otp_code_retriever; sourceTree = ""; @@ -3260,6 +3460,7 @@ E2DDF1B22B6A9E1D00E9FAB7 /* MSALNativeAuthCustomErrorSerializerTests.swift */, E2F8900D2B75546A001FBC7C /* MSALNativeAuthUnknownCaseProtocolTests.swift */, 289C1D8E2DE8C669009EEBEA /* MSALNativeAuthInternalConfigurationTest.swift */, + 4E0AA7DA6DFC949218EEA54E /* v2 */, ); path = network; sourceTree = ""; @@ -3495,6 +3696,55 @@ path = result; sourceTree = ""; }; + 4583FB32E02B0075F72EB043 /* v2 */ = { + 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 */, + 36D4FF97FB100CCD85295702 /* MSALNativeAuthV2ResponseErrorHandler.swift */, + 1A153D161101EAF09A32E906 /* parser */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + 4E0AA7DA6DFC949218EEA54E /* v2 */ = { + isa = PBXGroup; + children = ( + C52FC1A535E843CF897CC543 /* MSALNativeAuthV2ResponseParserTests.swift */, + 16CF356DB03BC6B9F96B91E9 /* MSALNativeAuthV2HrefURLResolverTests.swift */, + C74AE8A04459BC8C4405B7CD /* MSALNativeAuthV2ParametersTests.swift */, + 94DBF7076275DC959B195094 /* MSALNativeAuthV2RequestProviderTests.swift */, + 5C52731D0550F3D98B606302 /* MSALNativeAuthFlowErrorTests.swift */, + 065DECC57E9CF4618C1D5494 /* MSALNativeAuthV2HALResponseSerializerTests.swift */, + 26A54EBC67992C2F90976346 /* MSALNativeAuthV2ResponseErrorHandlerTests.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 = ( @@ -3512,17 +3762,6 @@ path = state; sourceTree = ""; }; - F58967D39C050697CB65B47C /* v2 */ = { - isa = PBXGroup; - children = ( - 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, - B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, - 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, - 58F36BCB7532CC3D9D60EE3A /* state */, - ); - path = v2; - sourceTree = ""; - }; 91AA245E2BDF6441005037EA /* Preview Content */ = { isa = PBXGroup; children = ( @@ -3555,6 +3794,17 @@ path = resources; sourceTree = ""; }; + 9279009AB4669F3B2A4D66FD /* v2 */ = { + isa = PBXGroup; + children = ( + 657374069BB444E4D7FF440C /* MSALNativeAuthFlowControllerMock.swift */, + 4F81BEE5A1780C77F88EFC54 /* MSALNativeAuthV2RequestProviderMock.swift */, + 9E43920225999E4C62456A92 /* MSALNativeAuthV2ResponseParserMock.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; 94E876C91E492D2800FB96ED /* instance */ = { isa = PBXGroup; children = ( @@ -4516,6 +4766,7 @@ E243F69229D1973900DAC60F /* sign_up */, DEE34F51D170B71C00BC302A /* reset_password */, DE0FECAA2993AD3700B139A8 /* MSALNativeAuthResendCodeRequestResponse.swift */, + 4583FB32E02B0075F72EB043 /* v2 */, ); path = responses; sourceTree = ""; @@ -5268,6 +5519,8 @@ DEDB29A229DDA992008DA85B /* errors */, E235613329C9D528000E01CA /* MSALNativeAuthInternalChallengeType.swift */, 289C1D8B2DE899B7009EEBEA /* MSALNativeAuthInternalCapability.swift */, + 03D8CB5A8588FA78B92F2A2D /* v2 */, + B8D6FE2555B1F9449776DB7A /* MSALNativeAuthRequestInterceptorBridge.swift */, ); path = network; sourceTree = ""; @@ -5367,6 +5620,7 @@ 9B4EE9CD2A1686A900F243C1 /* MSALNativeAuthResetPasswordControllerTests.swift */, DE14096C2A38DF40008E6F1E /* MSALNativeAuthCredentialsControllerTests.swift */, 28A600A92C78E09F00455666 /* MSALNativeAuthMFAControllerTests.swift */, + E6E09369971B71B5529C8CFE /* v2 */, ); path = controllers; sourceTree = ""; @@ -5379,6 +5633,27 @@ path = factories; sourceTree = ""; }; + E6E09369971B71B5529C8CFE /* v2 */ = { + isa = PBXGroup; + children = ( + 76FDC0929F7E8268E1076A6F /* MSALNativeAuthFlowControllerTests.swift */, + 0EA741A0AB71BF04C23FD120 /* MSALNativeAuthFlowResponseDispatcherTests.swift */, + ); + name = v2; + path = v2; + sourceTree = ""; + }; + F58967D39C050697CB65B47C /* v2 */ = { + isa = PBXGroup; + children = ( + 87AA69B6347AED83F0C677E4 /* MSALNativeAuthFlowError.swift */, + B7081D4EEFA60AF1D7938C1C /* MSALNativeAuthFlowDelegate.swift */, + 88219AB3EAB46203CD9729B9 /* MSALNativeAuthFlowScenario.swift */, + 58F36BCB7532CC3D9D60EE3A /* state */, + ); + path = v2; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -6819,7 +7094,6 @@ DED1F0A12DD64544009CB97A /* MSALNativeAuthSignInJITEndToEndTests.swift in Sources */, AE64B3751432B2A8DD6C7FAB /* MailTMConstants.swift in Sources */, 64463489E8DC5172D49F98FF /* MailTMHTTPClient.swift in Sources */, - 31A0E8B0B69F886271896E3E /* RetryExecutor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7265,6 +7539,46 @@ E2EFAD162A70300B00D6C3DE /* MSALNativeAuthControllerTelemetryWrapper.swift in Sources */, 285F58542C5BA33B00F4EFA4 /* MSALNativeAuthSignInIntrospectRequestParameters.swift in Sources */, 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 */, + 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 */, + 0F534648963730396C678674 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, + B39266A1EEC9C21B7686E148 /* MSALNativeAuthV2ParsedResponses.swift in Sources */, + 7211BE4BFD25184510F7DBAC /* MSALNativeAuthV2ResponseParser.swift in Sources */, + 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 */, + 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 */, + E1B065322ACBAB3B09BDAE5F /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, + 55E13C0C6C914BAED172AD0C /* MSALNativeAuthTokenRequestHandling.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7568,6 +7882,46 @@ DE8DC4612C66219600534E8F /* SignUpResults.swift in Sources */, DE8DC4972C6621A600534E8F /* SignInAfterResetPasswordDelegate.swift in Sources */, 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 */, + 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 */, + 961B634DFA8CCA52DD153AC1 /* MSALNativeAuthV2ResponseErrorHandler.swift in Sources */, + 49872D81F840B9D9270D9A3B /* MSALNativeAuthV2ParsedResponses.swift in Sources */, + 2DD57B8C07583D74E7F03024 /* MSALNativeAuthV2ResponseParser.swift in Sources */, + 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 */, + 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 */, + D661864E47CDC268A2FA5EEF /* MSALNativeAuthRequestInterceptorBridge.swift in Sources */, + 358F769C7CC02B687DA46452 /* MSALNativeAuthTokenRequestHandling.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7758,6 +8112,18 @@ 9BD2765B2A0E7E7D00FBD033 /* ResetPasswordCodeSentStateTests.swift in Sources */, E2F626B32A781CE300C4A303 /* SignInDelegatesSpies.swift in Sources */, E22427EA2B065EAE0006C55E /* SignUpVerifyCodeDelegateDispatcherTests.swift in Sources */, + A370E8BCE6A05E05ECC63027 /* MSALNativeAuthFlowControllerMock.swift in Sources */, + 5E471E84AA33CFA840BBA964 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, + D1196AE2B1E112D81628C479 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */, + E04298BA8ED8FBE431F561A2 /* MSALNativeAuthFlowControllerTests.swift in Sources */, + A89E21F4CDFA919F513EA87E /* MSALNativeAuthV2ResponseParserTests.swift in Sources */, + 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; }; @@ -7936,6 +8302,18 @@ 28FB1BB62E0AF1F90065B784 /* MSALNativeAuthPublicClientApplicationConfigObjCTest.m in Sources */, 289E44BA2C9D843F00F6B9D7 /* MFARequestChallengeErrorTests.swift in Sources */, DE8DC56C2C66221C00534E8F /* MSALNativeLoggingTests.swift in Sources */, + A939579E9B632F2EFA0447E6 /* MSALNativeAuthFlowControllerMock.swift in Sources */, + 84AEAFD45E4487CB1A9F8751 /* MSALNativeAuthV2RequestProviderMock.swift in Sources */, + F9BA2A6AA026A96533600735 /* MSALNativeAuthV2ResponseParserMock.swift in Sources */, + 9D57981C31A9157AF52A29B9 /* MSALNativeAuthFlowControllerTests.swift in Sources */, + A509294FE137EA2B29C6AE24 /* MSALNativeAuthV2ResponseParserTests.swift in Sources */, + 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; }; @@ -8002,7 +8380,6 @@ DED1F0A02DD64544009CB97A /* MSALNativeAuthSignInJITEndToEndTests.swift in Sources */, 022239DBCF2EF4AD83359DD3 /* MailTMConstants.swift in Sources */, 9313B1799984552C778C5E5C /* MailTMHTTPClient.swift in Sources */, - 0B808ECA169C3107F4335691 /* RetryExecutor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -8445,7 +8822,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; @@ -8590,7 +8967,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; @@ -8608,7 +8985,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; @@ -8663,7 +9040,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; @@ -8680,7 +9057,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; }; @@ -9357,7 +9734,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)", @@ -9386,7 +9763,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)", @@ -9413,7 +9790,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", @@ -9435,7 +9812,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", @@ -9459,7 +9836,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)", @@ -9491,7 +9868,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)", @@ -9520,7 +9897,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; @@ -9541,7 +9918,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; @@ -9663,7 +10040,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; @@ -9734,7 +10111,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/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/MSALNativeAuthBaseController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift index a8b3ffb276..938c1068a3 100644 --- a/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthBaseController.swift @@ -35,6 +35,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 @@ -153,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/MSALNativeAuthTokenController.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenController.swift index 4c9d4f13ef..ed3670049a 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 @@ -61,16 +61,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, @@ -132,104 +122,12 @@ 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 result - } -} - -// 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 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 - ) 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)) - } - } + return try cacheAccessor.cache( + tokenResponse, + context: context, + msidConfiguration: msidConfiguration + ) { [responseValidator] tokenResult, accountIdentifier in + try responseValidator.validateAccount(with: tokenResult, context: context, accountIdentifier: accountIdentifier) } } } diff --git a/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift new file mode 100644 index 0000000000..63e889afa2 --- /dev/null +++ b/MSAL/src/native_auth/controllers/MSALNativeAuthTokenRequestHandling.swift @@ -0,0 +1,69 @@ +// +// 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 + +protocol MSALNativeAuthTokenRequestHandling { + + func performTokenRequest( + _ request: MSIDHttpRequest, + context: MSIDRequestContext + ) async -> Result +} + +extension MSALNativeAuthTokenRequestHandling { + + func performTokenRequest( + _ request: MSIDHttpRequest, + context: MSIDRequestContext + ) async -> Result { + let requestCorrelationId = request.context?.correlationId().uuidString + let result: Result = await withCheckedContinuation { continuation in + request.send { response, error in + if let error = error { + continuation.resume(returning: .failure(error)) + } 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) + } + } + } +} diff --git a/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift b/MSAL/src/native_auth/controllers/factories/MSALNativeAuthControllerFactory.swift index 30052d83d9..cbe7690aaf 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 makeFlowController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthFlowControlling } final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { @@ -56,4 +57,8 @@ final class MSALNativeAuthControllerFactory: MSALNativeAuthControllerBuildable { func makeCredentialsController(cacheAccessor: MSALNativeAuthCacheInterface) -> MSALNativeAuthCredentialsControlling { return MSALNativeAuthCredentialsController(config: config, cacheAccessor: cacheAccessor) } + + func makeFlowController(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..86f7908197 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowContinuationState.swift @@ -0,0 +1,69 @@ +// +// 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 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 the SDK must +/// follow to advance the server-driven flow. +class MSALNativeAuthFlowContinuationState { + let flowScenario: MSALNativeAuthFlowScenario + let continuationToken: String? + let links: [MSALNativeAuthV2LinkKey: URL] + let username: String? + /// 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?, + links: [MSALNativeAuthV2LinkKey: URL], + username: String?, + scopes: [String] = [] + ) { + self.flowScenario = flowScenario + self.continuationToken = continuationToken + self.links = links + self.username = username + self.scopes = scopes + } + + func link(_ relation: MSALNativeAuthV2LinkRelation) -> URL? { + return links[.relation(relation)] + } + + /// The challenge / enroll link associated with a specific auth method. + func methodLink(for methodId: String) -> URL? { + 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 new file mode 100644 index 0000000000..88f60f4fce --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowController.swift @@ -0,0 +1,672 @@ +// +// 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 + +/// 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 { + + private let config: MSALNativeAuthInternalConfiguration + private let requestProvider: MSALNativeAuthV2RequestProviding + private let responseParser: MSALNativeAuthV2ResponseParsing + private let resultFactory: MSALNativeAuthResultBuildable + private let cacheAccessor: MSALNativeAuthCacheInterface + + private let kNumberOfTimesToRetryPollCompletionCall = 5 + // TODO: Confirm this is needed and server doesn't send + private let pollIntervalSeconds: Double = 1.5 // delay between poll attempts + + init( + config: MSALNativeAuthInternalConfiguration, + requestProvider: MSALNativeAuthV2RequestProviding, + responseParser: MSALNativeAuthV2ResponseParsing, + cacheAccessor: MSALNativeAuthCacheInterface, + resultFactory: MSALNativeAuthResultBuildable + ) { + self.config = config + self.requestProvider = requestProvider + self.responseParser = responseParser + self.resultFactory = resultFactory + self.cacheAccessor = cacheAccessor + super.init(clientId: config.clientId) + } + + convenience init(config: MSALNativeAuthInternalConfiguration, cacheAccessor: MSALNativeAuthCacheInterface) { + self.init( + config: config, + requestProvider: MSALNativeAuthV2RequestProvider(config: config), + responseParser: MSALNativeAuthV2ResponseParser(), + cacheAccessor: cacheAccessor, + resultFactory: MSALNativeAuthResultFactory(config: config, cacheAccessor: cacheAccessor) + ) + } + + // MARK: - Entry points + + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse { + return notImplementedResponse(scenario: .signUp) + } + + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse { + return notImplementedResponse(scenario: .signIn) + } + + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse { + 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( + flowScenario: flowScenario, + apiId: .telemetryApiIdV2ResetPasswordStart, + context: context + ) + guard case .continuationToken(let continuationToken, let resetPasswordLink) = authorizationChallenge else { + return failure(authorizationChallenge, event: event, context: context, scenario: flowScenario) + } + + let startResult = await performInteraction(context: context) { + try self.requestProvider.resetPasswordStart( + username: parameters.username, + continuationToken: continuationToken, + href: resetPasswordLink, + apiId: .telemetryApiIdV2ResetPasswordStart, + context: context + ) + } + + guard case .challengeRequired(let challengeContinuationToken, let challengeHref, _) = startResult else { + return interactionFailure(startResult, event: event, context: context, scenario: flowScenario, newState: nil) + } + + let challengeResult = await performInteraction(context: context) { + try self.requestProvider.challenge( + href: challengeHref, + continuationToken: challengeContinuationToken, + apiId: .telemetryApiIdV2ResetPasswordStart, + context: context + ) + } + + let continuation = MSALNativeAuthFlowContinuationState( + flowScenario: flowScenario, + continuationToken: challengeContinuationToken, + links: [:], + username: parameters.username, + 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 flowContinuationState = state.continuation + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmitCode, context: context) + + guard let verifyHref = flowContinuationState.link(.verify)?.absoluteString else { + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing verify link")), + event: event, + 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.verify( + href: verifyHref, + otp: code, + continuationToken: continuationToken, + apiId: .telemetryApiIdV2ResetPasswordSubmitCode, + context: context + ) + } + 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 { + 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) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordSubmit, context: context) + let flowContinuationState = state.continuation + + guard let updateHref = flowContinuationState.link(.update)?.absoluteString else { + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing update link")), + event: event, + 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 updateResult = await performInteraction(context: context) { + try self.requestProvider.updatePassword( + href: updateHref, + newPassword: password, + continuationToken: continuationToken, + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) + } + + 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 + ) + } + + let retryExecutor = MSALNativeAuthRetryExecutor(delays: [pollIntervalSeconds]) + let terminalPollResult = await retryExecutor.execute( + maxAttempts: kNumberOfTimesToRetryPollCompletionCall + ) { () -> MSALNativeAuthV2InteractionParsedResponse? in + let pollResult = await performInteraction(context: context) { + try self.requestProvider.poll( + href: pollHref, + continuationToken: pollToken, + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) + } + + if case .pollInProgress(let token, _) = pollResult { + pollToken = token + return nil + } + + return pollResult + } + + guard let terminalPollResult = terminalPollResult else { + return failure( + .error( + MSALNativeAuthFlowError( + type: .generalError, + errorDescription: "Password reset did not complete in time" + ) + ), + event: event, + context: context, scenario: flowContinuationState.flowScenario + ) + } + + guard case .readyToComplete(let completionToken) = terminalPollResult else { + return interactionFailure( + terminalPollResult, + event: event, + context: context, + scenario: flowContinuationState.flowScenario, + newState: nil + ) + } + + 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 { + return notImplementedResponse(scenario: state.continuation.flowScenario) + } + + func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + state: MSALNativeAuthFlowInternalState + ) async -> MSALNativeAuthFlowControllerResponse { + return notImplementedResponse(scenario: state.continuation.flowScenario) + } + + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return notImplementedResponse(scenario: state.continuation.flowScenario) + } + + func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + let context = MSALNativeAuthRequestContext(correlationId: nil) + let event = makeAndStartTelemetryEvent(id: .telemetryApiIdV2ResetPasswordResendCode, context: context) + let flowContinuationState = state.continuation + + guard let resendHref = flowContinuationState.link(.resend)?.absoluteString else { + return failure( + .error(MSALNativeAuthFlowError(type: .generalError, errorDescription: "Missing resend link")), + event: event, + 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: continuationToken, + apiId: .telemetryApiIdV2ResetPasswordResendCode, + context: context + ) + } + + let step = MSALNativeAuthFlowStepContext(apiId: .telemetryApiIdV2ResetPasswordResendCode, event: event, context: context) + return handleResendCodeResult(result, flowContinuationState: flowContinuationState, step: step) + } + + // MARK: - Shared step helpers + + private func performAuthorizeChallengeStart( + flowScenario: MSALNativeAuthFlowScenario, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) async -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { + let result: Result = await send { + try self.requestProvider.authorizeChallengeStart(apiId: apiId, context: context) + } + return responseParser.parseAuthorizeChallenge(context: context, result, flowScenario: flowScenario) + } + + private func performAuthorizeChallengeContinue( + flowScenario: MSALNativeAuthFlowScenario, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) async -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { + let result: Result = await send { + try self.requestProvider.authorizeChallengeContinue(continuationToken: continuationToken, apiId: apiId, context: context) + } + return responseParser.parseAuthorizeChallenge(context: context, result, flowScenario: flowScenario) + } + + private func performInteraction( + context: MSALNativeAuthRequestContext, + requestBuilder: @escaping () throws -> MSIDHttpRequest + ) async -> MSALNativeAuthV2InteractionParsedResponse { + let result: Result = await send(requestBuilder) + return responseParser.parseInteraction(context: context, 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 + + /// Maps the challenge response from the reset-password start sequence. + func handleChallengeResult( + _ result: MSALNativeAuthV2InteractionParsedResponse, + 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(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 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 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(step.event, context: step.context, error: error) + return response( + .error(error: error, newState: error.isInvalidCode || error.type == .invalidPassword ? recoverableState : nil), + context: step.context, scenario: flowContinuationState.flowScenario + ) + default: + return interactionFailure(result, event: step.event, context: step.context, scenario: flowContinuationState.flowScenario, newState: nil) + } + } + + // 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( + flowContinuationState: MSALNativeAuthFlowContinuationState, + continuationToken: String, + step: MSALNativeAuthFlowStepContext + ) async -> MSALNativeAuthFlowControllerResponse { + let codeResult = await performAuthorizeChallengeContinue( + flowScenario: flowContinuationState.flowScenario, + continuationToken: continuationToken, + apiId: step.apiId, + context: step.context + ) + guard case .authorizationCode(let code) = codeResult else { + return failure(codeResult, event: step.event, context: step.context, scenario: flowContinuationState.flowScenario) + } + + 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: step.context, msidConfiguration: msidConfiguration) + + guard let accountResult = resultFactory.makeUserAccountResult(tokenResult: tokenResult, context: step.context) else { + let error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unable to construct account result") + stopTelemetryEvent(step.event, context: step.context, error: error) + return response(.error(error: error, newState: nil), context: step.context, scenario: flowContinuationState.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(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(step.event, context: step.context, error: flowError) + return response(.error(error: flowError, newState: nil), context: step.context, scenario: flowContinuationState.flowScenario) + } + } + + /// Builds the `/token` request and delegates the send/parse to the shared token-request handler. + 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, apiId: apiId, context: context) + } catch { + return .failure(error) + } + + return await performTokenRequest(request, context: context).map { $0 as MSIDTokenResponse } + } + + private func cacheTokenResponse( + _ tokenResponse: MSIDTokenResponse, + context: MSALNativeAuthRequestContext, + msidConfiguration: MSIDConfiguration + ) throws -> MSIDTokenResult { + return try cacheAccessor.cache( + tokenResponse, + context: context, + msidConfiguration: msidConfiguration + ) { tokenResult, accountIdentifier in + try self.validateAccount(tokenResult, accountIdentifier: accountIdentifier, context: context) + } + } + + 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 validAccount + } + + /// Extracts the granted scopes from a token response so the cache target matches what was issued. + private func retrieveScopes(from tokenResponse: MSIDTokenResponse) -> [String] { + guard let scope = tokenResponse.scope, !scope.isEmpty else { + return [] + } + return scope.components(separatedBy: " ").filter { !$0.isEmpty } + } + + /// 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 { + if let href = href, let url = try? resolver.url(forHref: href) { + resolvedLinks[.relation(relation)] = url + } + } + return resolvedLinks + } + + // MARK: - Response construction + + private func response( + _ result: MSALNativeAuthFlowResult, + context: MSALNativeAuthRequestContext, + scenario: MSALNativeAuthFlowScenario + ) -> MSALNativeAuthFlowControllerResponse { + return MSALNativeAuthFlowControllerResponse( + result, + correlationId: context.correlationId(), + scenario: scenario + ) + } + + /// 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() + ) + } + + /// 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( + _ parsed: MSALNativeAuthV2AuthorizeChallengeParsedResponse, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext, + scenario: MSALNativeAuthFlowScenario + ) -> MSALNativeAuthFlowControllerResponse { + let error: MSALNativeAuthFlowError + if case .error(let flowError) = parsed { + error = flowError + } else { + error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected authorize-challenge response") + } + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: nil), context: context, scenario: scenario) + } + + private func interactionFailure( + _ parsed: MSALNativeAuthV2InteractionParsedResponse, + event: MSIDTelemetryAPIEvent?, + context: MSALNativeAuthRequestContext, + scenario: MSALNativeAuthFlowScenario, + newState: MSALNativeAuthFlowInternalState? + ) -> MSALNativeAuthFlowControllerResponse { + let error: MSALNativeAuthFlowError + if case .error(let flowError) = parsed { + error = flowError + } else { + error = MSALNativeAuthFlowError(type: .generalError, errorDescription: "Unexpected server response") + } + stopTelemetryEvent(event, context: context, error: error) + return response(.error(error: error, newState: newState), context: context, scenario: scenario) + } +} 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/MSALNativeAuthFlowControlling.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControlling.swift new file mode 100644 index 0000000000..8672d35379 --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowControlling.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 ``MSALNativeAuthFlowControllerResponse``. +protocol MSALNativeAuthFlowControlling { + + // MARK: - Entry points + + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse + + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse + + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse + + // MARK: - Continuation + + func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + + func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + + func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + state: MSALNativeAuthFlowInternalState + ) async -> MSALNativeAuthFlowControllerResponse + + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + + func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse +} diff --git a/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.swift new file mode 100644 index 0000000000..5941158dad --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowInternalState.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 + +/// Internal state that continues a Native Auth V2 (server-driven) flow. +/// +/// 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 MSALNativeAuthFlowInternalState { + + let continuation: MSALNativeAuthFlowContinuationState + private let controller: MSALNativeAuthFlowControlling + private let dispatcher = MSALNativeAuthFlowResponseDispatcher() + + init(continuation: MSALNativeAuthFlowContinuationState, controller: MSALNativeAuthFlowControlling) { + self.continuation = continuation + self.controller = controller + } + + /// 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 (MSALNativeAuthFlowControlling, MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + ) { + Task { + let response = await operation(controller, self) + await dispatcher.dispatch(response, delegate: delegate) + } + } +} 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..334edf220e --- /dev/null +++ b/MSAL/src/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcher.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 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 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 state): + await dispatchActionRequired(state, 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 error = MSALNativeAuthFlowError( + type: .browserRequired, + correlationId: response.correlationId + ) + await delegate.onFlowError(error: error, scenario: scenario) + response.telemetryUpdate?(.success(())) + } + } + + private func dispatchActionRequired( + _ state: MSALNativeAuthState, + response: MSALNativeAuthFlowControllerResponse, + delegate: MSALNativeAuthFlowDelegate + ) async { + 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 MSALNativeAuthNewPasswordRequiredState: + await deliver(to: delegate, response: response, as: MSALNativeAuthNewPasswordRequiredDelegate.self, scenario: scenario) { + await $0.onNewPasswordRequired(state: state, scenario: scenario) + } + default: + await notImplemented(delegate: delegate, scenario: scenario, correlationId: response.correlationId) + } + } + + /// Invokes the app's per-state callback when the delegate conforms to `Delegate`; otherwise + /// reports `notImplemented` through the error callback. + private func deliver( + to delegate: MSALNativeAuthFlowDelegate, + response: MSALNativeAuthFlowControllerResponse, + as delegateType: Delegate.Type, + scenario: MSALNativeAuthFlowScenario, + callback: (Delegate) async -> Void + ) async { + 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..329e6e62a4 --- /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.allCases + + /// 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(state: MSALNativeAuthState) + case completed(MSALNativeAuthUserAccountResult) + case error(error: MSALNativeAuthFlowError, newState: MSALNativeAuthFlowInternalState?) + case browserRequired +} 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/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/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..713d70cbca --- /dev/null +++ b/MSAL/src/native_auth/network/MSALNativeAuthRequestInterceptorBridge.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. + +@_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/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/errors/MSALNativeAuthErrorMessage.swift b/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift index ceb577e229..24958a135f 100644 --- a/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift +++ b/MSAL/src/native_auth/network/errors/MSALNativeAuthErrorMessage.swift @@ -40,7 +40,6 @@ enum MSALNativeAuthErrorMessage { 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/HALResource.swift b/MSAL/src/native_auth/network/responses/v2/HALResource.swift new file mode 100644 index 0000000000..99ff4dac7e --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/HALResource.swift @@ -0,0 +1,117 @@ +// +// 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 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/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..5458ddef88 --- /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 + +final class MSALNativeAuthHALChallengeResponse: MSALNativeAuthHALResponse { + + /// 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] + } + } + + /// `_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 new file mode 100644 index 0000000000..58c1623ca3 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthHALResponse.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. + +@_implementationOnly import MSAL_Private + +/// 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 { + let code: String? + let message: String? + let innerErrorCode: String? + let correlationId: UUID? + } + + let statusCode: Int + var correlationId: UUID? + + let continuationToken: String? + + let links: [String: String] + + let error: ServerError? + + 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? { + return links[relation] + } + + func href(for relation: MSALNativeAuthV2LinkRelation) -> String? { + return links[relation.rawValue] + } +} 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/MSALNativeAuthV2HALAction.swift b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.swift new file mode 100644 index 0000000000..01224eaada --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALAction.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. + +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 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 new file mode 100644 index 0000000000..bcae7ca811 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializer.swift @@ -0,0 +1,268 @@ +// +// 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 + +/// 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 +/// 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 { + let statusCode = httpResponse?.statusCode ?? 0 + let correlationId = MSALNativeAuthHALResponse.retrieveCorrelationIdFromHeaders(from: httpResponse) + + guard let data = data, !data.isEmpty else { + return MSALNativeAuthHALResponse( + statusCode: statusCode, + correlationId: correlationId, + continuationToken: nil, + links: [:], + error: nil, + isWebFallbackRequired: false + ) + } + + 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 = HALResource(json: json) + let state = resource.string(forKey: "state") + let error = parseError(from: json, fallbackCorrelationId: correlationId) + let base = BaseFields( + statusCode: statusCode, + correlationId: correlationId, + continuationToken: resource.string(forKey: "continuationToken") ?? resource.string(forKey: "continuation_token"), + links: parseLinks(from: resource, json: json), + 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) + } + + 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" { + 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 + ) + } + + 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 { + result[relation] = 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 + } + + private func parseMethods(from resource: HALResource) -> [MSALNativeAuthHALChallengeResponse.EmbeddedMethod] { + let methodResources = resource.embeddedResources(rel: "methods") + return methodResources.map { dict in + let methodResource = HALResource(json: dict) + var links: [String: String] = [:] + for (relation, halLinks) in methodResource.links { + if let href = halLinks.first?.href { + links[relation] = href + } + } + return MSALNativeAuthHALChallengeResponse.EmbeddedMethod( + id: methodResource.string(forKey: "id"), + type: methodResource.string(forKey: "type"), + hint: methodResource.string(forKey: "hint"), + links: links + ) + } + } + + private 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..ab98313c25 --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/MSALNativeAuthV2ResponseErrorHandler.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. + +@_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 `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 { + + // swiftlint:disable:next function_parameter_count + 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/parser/MSALNativeAuthV2ParsedResponses.swift b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ParsedResponses.swift new file mode 100644 index 0000000000..b19901dafb --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ParsedResponses.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 + +/// 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) + /// 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, lHref), .continuationToken(rToken, rHref)): + return lToken == rToken && lHref == rHref + case let (.authorizationCode(lCode), .authorizationCode(rCode)): + return lCode == rCode + case let (.error(lError), .error(rError)): + return lError.type == rError.type + default: + return false + } + } +} + +/// Parsed outcome of an SSPR interaction step (resetpassword start / challenge / verify / update / poll). +/// +/// A single enum represents every HAL interaction response; the parser selects the case +/// from the HAL `state` / `action` pair. +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. + case codeRequired(continuationToken: String, verifyHref: String, resendHref: String?, sentTo: String, channelType: MSALNativeAuthChannelType, 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) + /// `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 { + 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, 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 (.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 (.browserRequired, .browserRequired): + return true + case let (.error(lError), .error(rError)): + return lError.type == rError.type + default: + return false + } + } +} diff --git a/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift new file mode 100644 index 0000000000..dfff9a7bee --- /dev/null +++ b/MSAL/src/native_auth/network/responses/v2/parser/MSALNativeAuthV2ResponseParser.swift @@ -0,0 +1,275 @@ +// +// 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 + +/// Maps a raw ``MSALNativeAuthHALResponse`` (or transport error) into a parsed, controller-facing response. +protocol MSALNativeAuthV2ResponseParsing { + func parseAuthorizeChallenge( + context: MSIDRequestContext, + _ result: Result, + flowScenario: MSALNativeAuthFlowScenario + ) -> MSALNativeAuthV2AuthorizeChallengeParsedResponse + func parseInteraction( + context: MSIDRequestContext, + _ result: Result + ) -> MSALNativeAuthV2InteractionParsedResponse +} + +final class MSALNativeAuthV2ResponseParser: MSALNativeAuthV2ResponseParsing { + + func parseAuthorizeChallenge( + context: MSIDRequestContext, + _ result: Result, + flowScenario: MSALNativeAuthFlowScenario + ) -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { + 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)) + } + if let code = (response as? MSALNativeAuthHALAuthorizationCodeResponse)?.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" + )) + } + } + + func parseInteraction( + context: MSIDRequestContext, + _ result: Result + ) -> MSALNativeAuthV2InteractionParsedResponse { + switch result { + 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") + // The URL is not returned here as the developer needs to invoke Auth UX + return .browserRequired + } + if let error = response.error { + return .error(flowError(from: error, context: context)) + } + + if response is MSALNativeAuthHALReadyToCompleteResponse { + 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")) + } + + return parseInteractionResponse(response, continuationToken: continuationToken, context: context) + } + } + + 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")) + } + } +} + +extension MSALNativeAuthV2ResponseParser { + + // 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 func missingLink( + _ relation: MSALNativeAuthV2LinkRelation, + context: MSIDRequestContext + ) -> MSALNativeAuthV2InteractionParsedResponse { + 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, context: MSIDRequestContext) -> MSALNativeAuthFlowError { + let message = serverError.message + let errorCodes = estsErrorCodes(from: message) + let innerErrorCode = serverError.innerErrorCode + let type: MSALNativeAuthFlowError.ErrorType + + 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 + // surfaces as a general error. + type = serverError.code == "invalidGrant" ? .invalidCode : .generalError + } else if let message = message, message.contains("AADSTS50034") { + type = .userNotFound + } 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 = .invalidCredentials + } else if serverError.code == "invalidGrant" { + type = .invalidCode + } else { + type = .generalError + } + + logServerError(serverError, type: type, context: context) + + return MSALNativeAuthFlowError( + type: type, + errorDescription: message, + errorCodes: errorCodes, + correlationId: serverError.correlationId ?? UUID() + ) + } + + 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)) + } + + private 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 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/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/MSALNativeAuthV2HrefURLResolver.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift new file mode 100644 index 0000000000..5f540448c8 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2HrefURLResolver.swift @@ -0,0 +1,141 @@ +// +// 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 ``MSALNativeAuthEndpoint`` cases. The slice/data-center query +/// parameter is appended consistently. + +// TODO: Update based on API changes +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 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 + } + 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: 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. + guard let hrefComponents = URLComponents(string: normalizedHref(from: trimmed)) else { + throw MSALNativeAuthInternalError.invalidUrl + } + + guard var components = URLComponents(url: authorityURL, resolvingAgainstBaseURL: true) else { + throw MSALNativeAuthInternalError.invalidUrl + } + + // 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 authorization challenge. + 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. + 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 + + // Drop a leading `{tenant}` placeholder segment if present. + if result.hasPrefix("{tenant}") { + result = String(result.dropFirst("{tenant}".count)) + } + + // 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 result + } + + 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/MSALNativeAuthV2LinkRelation.swift b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift new file mode 100644 index 0000000000..6691b9e04b --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2LinkRelation.swift @@ -0,0 +1,62 @@ +// +// 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`). 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/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 new file mode 100644 index 0000000000..ebe04310aa --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBody.swift @@ -0,0 +1,44 @@ +// +// 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 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] = [:] + + if let continuationToken = continuationToken { + body[MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue] = continuationToken + } + + return body + } +} 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..f1110a050b --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestBodyKey.swift @@ -0,0 +1,38 @@ +// +// 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) +enum MSALNativeAuthV2RequestBodyKey: String { + case username + case continuationToken + case code + case otp + case newPassword +} 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..d76806b154 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestConfigurator.swift @@ -0,0 +1,96 @@ +// +// 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 + +/// 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 + 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 + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = parameters.httpMethod + request.urlRequest = urlRequest + + request.requestSerializer = MSALNativeAuthUrlRequestSerializer( + context: parameters.context, + encoding: parameters.encoding, + body: parameters.body + ) + + // Reuse the shared AAD request pipeline. + 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 = MSALNativeAuthRequestInterceptorBridge(interceptor: interceptor) + } + + return request + } +} 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..0336257f49 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/MSALNativeAuthV2RequestProvider.swift @@ -0,0 +1,208 @@ +// +// 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 + +protocol MSALNativeAuthV2RequestProviding { + + /// SSPR entry, posted to the authorize-challenge `reset_password` href. + func resetPasswordStart(username: String, + continuationToken: String, + href: 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 + + /// Verify OTP (server `verify` href). + func verify(href: String, + otp: String, + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest + + /// Update password (server `update` href, PUT). + 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(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 +} + +final class MSALNativeAuthV2RequestProvider: MSALNativeAuthV2RequestProviding { + + private let config: MSALNativeAuthInternalConfiguration + private let configurator: MSALNativeAuthV2RequestConfigurator + + init(config: MSALNativeAuthInternalConfiguration) { + self.config = config + self.configurator = MSALNativeAuthV2RequestConfigurator(config: config) + } + + 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: apiId, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, + username: username, + continuationToken: continuationToken + )) + } + + 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: apiId, + operationType: MSALNativeAuthV2OperationType.challenge.rawValue, + requestBody: MSALNativeAuthV2ChallengeRequestBody(continuationToken: continuationToken) + )) + } + + 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: apiId, + operationType: MSALNativeAuthV2OperationType.verify.rawValue, + requestBody: MSALNativeAuthV2VerifyRequestBody(continuationToken: continuationToken, otp: otp) + )) + } + + 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: apiId, + operationType: MSALNativeAuthV2OperationType.updatePassword.rawValue, + requestBody: MSALNativeAuthV2UpdatePasswordRequestBody(continuationToken: continuationToken, newPassword: newPassword) + )) + } + + 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: apiId, + operationType: MSALNativeAuthV2OperationType.poll.rawValue, + requestBody: MSALNativeAuthV2PollRequestBody(continuationToken: continuationToken) + )) + } + + 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, + 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, + apiId: apiId + )) + } +} 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/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2AuthorizeChallengeContinueParameters.swift new file mode 100644 index 0000000000..74cbd126d8 --- /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 apiId: MSALNativeAuthTelemetryApiId + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + 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..60f61fd128 --- /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 apiId: MSALNativeAuthTelemetryApiId + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + 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..aed026226c --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2EntryParameters.swift @@ -0,0 +1,48 @@ +// +// 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 [ + MSALNativeAuthV2RequestBodyKey.username.rawValue: username, + MSALNativeAuthV2RequestBodyKey.continuationToken.rawValue: 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..d83190e049 --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2HrefParameters.swift @@ -0,0 +1,44 @@ +// +// 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` +struct MSALNativeAuthV2HrefParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let href: String + let httpMethod: String + let apiId: MSALNativeAuthTelemetryApiId + let operationType: MSALNativeAuthOperationType + 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) + } +} 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..ad97794713 --- /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(MSALNativeAuthEndpoint) + 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..2801080d1c --- /dev/null +++ b/MSAL/src/native_auth/network/v2/parameters/MSALNativeAuthV2TokenParameters.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 + +/// `POST /token` authorization-code exchange. Form encoded, raw OAuth (non-HAL) response. +struct MSALNativeAuthV2TokenParameters: MSALNativeAuthV2Requestable { + let context: MSALNativeAuthRequestContext + let clientId: String + let code: String + let scopes: [String] + let apiId: MSALNativeAuthTelemetryApiId + let encoding: MSALNativeAuthUrlRequestEncoding = .wwwFormUrlEncoded + let operationType: MSALNativeAuthOperationType = MSALNativeAuthV2OperationType.token.rawValue + let expectsRawJSONResponse = true + + var body: [AnyHashable: Any] { + var form: [AnyHashable: Any] = [ + MSALNativeAuthRequestParametersKey.grantType.rawValue: "authorization_code", + MSALNativeAuthV2RequestBodyKey.code.rawValue: 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/MSALNativeAuthPublicClientApplication.swift b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift index 50a70c381a..e48d2fd142 100644 --- a/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift +++ b/MSAL/src/native_auth/public/MSALNativeAuthPublicClientApplication.swift @@ -271,11 +271,12 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic parameters: MSALNativeAuthSignUpParametersV2, delegate: MSALNativeAuthFlowDelegate ) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: parameters.correlationId ?? UUID()), - scenario: .signUp - ) + Task { + let controller = controllerFactory.makeFlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + + let response = await controller.signUp(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) } } @@ -289,11 +290,12 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic parameters: MSALNativeAuthSignInParameters, delegate: MSALNativeAuthFlowDelegate ) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: parameters.correlationId ?? UUID()), - scenario: .signIn - ) + Task { + let controller = controllerFactory.makeFlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + + let response = await controller.signIn(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) } } @@ -307,11 +309,12 @@ public final class MSALNativeAuthPublicClientApplication: MSALPublicClientApplic parameters: MSALNativeAuthResetPasswordParametersV2, delegate: MSALNativeAuthFlowDelegate ) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: parameters.correlationId ?? UUID()), - scenario: .passwordReset - ) + Task { + let controller = controllerFactory.makeFlowController(cacheAccessor: cacheAccessor) + let dispatcher = MSALNativeAuthFlowResponseDispatcher() + + let response = await controller.resetPassword(parameters: parameters) + await dispatcher.dispatch(response, delegate: delegate) } } 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/MSALNativeAuthFlowError.swift b/MSAL/src/native_auth/public/state_machine/v2/MSALNativeAuthFlowError.swift index 7ca07c8fd4..d5f1508074 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. @@ -88,6 +86,23 @@ public class MSALNativeAuthFlowError: MSALNativeAuthError { ) } + /// 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 + ) + } + /// Describes why an error occurred and provides more information about the error. public override var errorDescription: String? { if let description = super.errorDescription { @@ -101,8 +116,6 @@ public class MSALNativeAuthFlowError: MSALNativeAuthError { return MSALNativeAuthErrorMessage.userNotFound case .invalidCode: return MSALNativeAuthErrorMessage.invalidCode - case .invalidContinuationToken: - return MSALNativeAuthErrorMessage.invalidContinuationToken case .invalidPassword: return MSALNativeAuthErrorMessage.invalidPassword case .invalidCredentials: @@ -143,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/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. 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 124502a0fc..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,18 +34,15 @@ 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. public func submitAttributes(_ attributes: [String: Any], delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.submitAttributes(attributes, state: state) } } 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 4cf2c8f774..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,18 +34,15 @@ 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. public func submitAttributes(_ attributes: [String: Any], delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.submitAttributes(attributes, state: state) } } 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 2d2b4886fe..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,30 +40,27 @@ 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. public func submitCode(_ code: String, delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + 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) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.resendCode(state: state) } } 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 e754d088cf..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. @@ -45,11 +45,8 @@ public class MSALNativeAuthMFARequiredState: MSALNativeAuthState { verificationContact: String?, delegate: MSALNativeAuthFlowDelegate ) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.selectAuthMethod(method, verificationContact: verificationContact, state: state) } } 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 210152322e..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,20 +40,17 @@ 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. public func submitChallenge(_ challenge: String, delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.submitChallenge(challenge, state: state) } } 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 index c714faea72..5497b7fd8a 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthNewPasswordRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthNewPasswordRequiredState.swift @@ -33,11 +33,8 @@ public class MSALNativeAuthNewPasswordRequiredState: MSALNativeAuthState { /// Submit a new password (self-service password reset). public func submitNewPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.submitNewPassword(password, state: state) } } 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 index d1e8260b04..4fe515e56c 100644 --- a/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthPasswordRequiredState.swift +++ b/MSAL/src/native_auth/public/state_machine/v2/state/MSALNativeAuthPasswordRequiredState.swift @@ -33,11 +33,8 @@ public class MSALNativeAuthPasswordRequiredState: MSALNativeAuthState { /// Submit a password. public func submitPassword(_ password: String, delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.submitPassword(password, state: state) } } 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 39982b2040..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 @@ -28,19 +28,30 @@ 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 -/// calling the method(s) exposed on that concrete state — each state exposes only the +/// (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. /// -/// 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. @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 + + init(internalState: MSALNativeAuthFlowInternalState) { + self.internalState = internalState + super.init() + } + + /// Forwards a continuation operation to the internal state. + func run( + delegate: MSALNativeAuthFlowDelegate, + operation: @escaping (MSALNativeAuthFlowControlling, MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse + ) { + 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 36c238cce5..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. @@ -45,11 +45,8 @@ public class MSALNativeAuthStrongAuthRegistrationRequiredState: MSALNativeAuthSt verificationContact: String?, delegate: MSALNativeAuthFlowDelegate ) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.selectAuthMethod(method, verificationContact: verificationContact, state: state) } } 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 9a38602281..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,20 +40,17 @@ 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. public func submitChallenge(_ challenge: String, delegate: MSALNativeAuthFlowDelegate) { - Task { @MainActor in - delegate.onFlowError( - error: MSALNativeAuthFlowError(type: .notImplemented, correlationId: UUID()), - scenario: self.scenario - ) + run(delegate: delegate) { controller, state in + await controller.submitChallenge(challenge, state: state) } } 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..ddb2b45ab4 100644 --- a/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift +++ b/MSAL/src/native_auth/telemetry/MSALNativeAuthTelemetryApiId.swift @@ -61,4 +61,19 @@ enum MSALNativeAuthTelemetryApiId: Int { case telemetryApiIdJITChallenge = 75030 case telemetryApiIdJITContinue = 75031 case telemetryApiISignInAfterJIT = 75032 + // 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 } 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() } diff --git a/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift new file mode 100644 index 0000000000..e1a89a1f45 --- /dev/null +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowControllerTests.swift @@ -0,0 +1,368 @@ +// +// 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 + +// swiftlint:disable type_body_length file_length +final class MSALNativeAuthFlowControllerTests: MSALNativeAuthTestCase { + + private var sut: MSALNativeAuthFlowController! + private var requestProviderMock: MSALNativeAuthV2RequestProviderMock! + private var parserMock: MSALNativeAuthV2ResponseParserMock! + private var cacheAccessorMock: MSALNativeAuthCacheAccessorMock! + private var resultFactoryMock: MSALNativeAuthResultFactoryMock! + private let context = MSALNativeAuthRequestContext(correlationId: UUID()) + + override func setUpWithError() throws { + try super.setUpWithError() + + requestProviderMock = .init() + parserMock = .init() + cacheAccessorMock = .init() + resultFactoryMock = .init() + + sut = .init( + config: MSALNativeAuthConfigStubs.configuration, + requestProvider: requestProviderMock, + responseParser: parserMock, + cacheAccessor: cacheAccessorMock, + resultFactory: resultFactoryMock + ) + } + + // MARK: - Helpers + + private func makeState(links: [MSALNativeAuthV2LinkRelation: URL], continuationToken: String = "ct") -> MSALNativeAuthFlowInternalState { + let continuation = MSALNativeAuthFlowContinuationState( + flowScenario: .passwordReset, + continuationToken: continuationToken, + links: relationLinks(links), + username: "user@contoso.com" + ) + return MSALNativeAuthFlowInternalState(continuation: continuation, controller: sut) + } + + private func relationLinks(_ links: [MSALNativeAuthV2LinkRelation: URL]) -> [MSALNativeAuthV2LinkKey: URL] { + links.reduce(into: [:]) { result, entry in + result[.relation(entry.key)] = entry.value + } + } + + private func resetPasswordParameters() -> MSALNativeAuthResetPasswordParametersV2 { + let params = MSALNativeAuthResetPasswordParametersV2(username: "user@contoso.com") + return params + } + + // MARK: - resetPassword (happy path -> code required) + + func test_resetPassword_happyPath_returnsCodeRequired() async { + requestProviderMock.mockRequest() + parserMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") + ] + 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) + ] + + let response = await sut.resetPassword(parameters: resetPasswordParameters()) + + guard case .actionRequired(let state) = response.result else { + return XCTFail("Expected actionRequired, got \(response.result)") + } + guard state is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(state)") + } + XCTAssertTrue(requestProviderMock.authorizeChallengeStartCalled) + XCTAssertTrue(requestProviderMock.resetPasswordStartCalled) + XCTAssertTrue(requestProviderMock.challengeCalled) + } + + func test_resetPassword_whenAuthorizationChallengeFails_returnsError() async { + requestProviderMock.mockRequest() + parserMock.authorizeChallengeResponses = [.error(MSALNativeAuthFlowError(type: .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() + parserMock.authorizeChallengeResponses = [ + .continuationToken(continuationToken: "ct-authorization-challenge", href: "https://contoso.com/reset") + ] + parserMock.interactionResponses = [ + .error(MSALNativeAuthFlowError(type: .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() + parserMock.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 state) = response.result else { + return XCTFail("Expected actionRequired, got \(response.result)") + } + guard state is MSALNativeAuthNewPasswordRequiredState else { + return XCTFail("Expected newPasswordRequired state, got \(state)") + } + XCTAssertTrue(requestProviderMock.verifyCalled) + XCTAssertEqual(requestProviderMock.verifyHrefReceived, "https://contoso.com/verify") + } + + func test_submitCode_whenInvalidCode_returnsErrorWithRetryState() async { + requestProviderMock.mockRequest() + parserMock.interactionResponses = [ + .error(MSALNativeAuthFlowError(type: .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() + parserMock.interactionResponses = [ + .pollInProgress(continuationToken: "ct-poll", pollHref: "https://contoso.com/poll"), + .readyToComplete(continuationToken: "ct-continue") + ] + parserMock.authorizeChallengeResponses = [ + .authorizationCode(code: "auth-code") + ] + cacheAccessorMock.expectedMSIDTokenResult = MSIDTokenResult() + 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) + } + + 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 { + requestProviderMock.mockRequest() + 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 response = await sut.resendCode(state: state) + + guard case .actionRequired(let state) = response.result else { + return XCTFail("Expected actionRequired, got \(response.result)") + } + guard state is MSALNativeAuthCodeRequiredState else { + return XCTFail("Expected codeRequired state, got \(state)") + } + XCTAssertTrue(requestProviderMock.challengeCalled) + } + + // MARK: - result handlers (branch logic) + + func test_handleChallenge_codeRequired_usesServerSentTo() async { + let state = await mapCodeRequired(sentTo: "u***@contoso.com") + XCTAssertEqual(state?.sentTo, "u***@contoso.com") + } + + func test_handleSubmitCode_error_invalidCode_isRecoverable() async { + let newState = await mapErrorNewState(type: .invalidCode) + XCTAssertNotNil(newState) + } + + func test_handleSubmitCode_error_invalidPassword_isRecoverable() async { + let newState = await mapErrorNewState(type: .invalidPassword) + XCTAssertNotNil(newState) + } + + func test_handleSubmitCode_error_generalError_isNotRecoverable() async { + let newState = await mapErrorNewState(type: .generalError) + XCTAssertNil(newState) + } + + func test_handleChallenge_browserRequired_returnsBrowserRequiredResult() async { + let response = await sut.handleChallengeResult(.browserRequired, flowContinuationState: makeFlow(), step: makeStep()) + guard case .browserRequired = response.result else { + return XCTFail("Expected browserRequired, got \(response.result)") + } + } + + private func mapCodeRequired(sentTo: String) async -> MSALNativeAuthCodeRequiredState? { + let response = await sut.handleChallengeResult( + .codeRequired( + continuationToken: "ct", + verifyHref: "https://contoso.com/verify", + resendHref: "https://contoso.com/resend", + sentTo: sentTo, + channelType: MSALNativeAuthChannelType(value: "email"), + codeLength: 8 + ), + flowContinuationState: makeFlow(), + step: makeStep() + ) + 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.handleSubmitCodeResult( + .error(MSALNativeAuthFlowError(type: type)), + flowContinuationState: makeFlow(), + step: makeStep(), + recoverableState: recoverableState + ) + guard case .error(_, let newState) = response.result else { + return nil + } + 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 + ) + } + +} 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..cd4c133cf4 --- /dev/null +++ b/MSAL/test/unit/native_auth/controllers/v2/MSALNativeAuthFlowResponseDispatcherTests.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 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, + 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: - 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 + } +} diff --git a/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift b/MSAL/test/unit/native_auth/mock/MSALNativeAuthFactoriesMocks.swift index 8d5de7eaf1..03fc859f97 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 = MSALNativeAuthFlowControllerMock() 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 makeFlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { + return v2FlowController + } } class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuildable { @@ -109,17 +114,20 @@ class MSALNativeAuthControllerProtocolFactoryMock: MSALNativeAuthControllerBuild var jitController: MSALNativeAuthJITControlling! var resetPasswordController: MSALNativeAuthResetPasswordControlling! var credentialsController: MSALNativeAuthCredentialsControlling! + var v2FlowController: MSALNativeAuthFlowControlling! init (signUpController: MSALNativeAuthSignUpControlling = MSALNativeAuthSignUpControllerMock(), signInController: MSALNativeAuthSignInControlling = MSALNativeAuthSignInControllerMock(), jitController: MSALNativeAuthJITControlling = MSALNativeAuthJITControllerMock(), resetPasswordController: MSALNativeAuthResetPasswordControlling = MSALNativeAuthResetPasswordControllerMock(), - credentialsController: MSALNativeAuthCredentialsControlling = MSALNativeAuthCredentialsControllerMock()) { + credentialsController: MSALNativeAuthCredentialsControlling = MSALNativeAuthCredentialsControllerMock(), + v2FlowController: MSALNativeAuthFlowControlling = MSALNativeAuthFlowControllerMock()) { 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 makeFlowController(cacheAccessor: MSAL.MSALNativeAuthCacheInterface) -> MSAL.MSALNativeAuthFlowControlling { + return v2FlowController + } } class MSALNativeAuthCacheAccessorFactoryMock: MSALNativeAuthCacheAccessorBuildable { diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.swift new file mode 100644 index 0000000000..235d94febb --- /dev/null +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthFlowControllerMock.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 MSALNativeAuthFlowControllerMock: MSALNativeAuthFlowControlling { + + var correlationId = UUID() + 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() -> MSALNativeAuthFlowControllerResponse { + return MSALNativeAuthFlowControllerResponse( + .error(error: MSALNativeAuthFlowError(type: .notImplemented), newState: nil), + correlationId: correlationId + ) + } + + func resetPassword(parameters: MSALNativeAuthResetPasswordParametersV2) async -> MSALNativeAuthFlowControllerResponse { + return resetPasswordResponse ?? notImplementedResponse() + } + + func signUp(parameters: MSALNativeAuthSignUpParametersV2) async -> MSALNativeAuthFlowControllerResponse { + return signUpResponse ?? notImplementedResponse() + } + + func signIn(parameters: MSALNativeAuthSignInParameters) async -> MSALNativeAuthFlowControllerResponse { + return signInResponse ?? notImplementedResponse() + } + + func submitCode(_ code: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return submitCodeResponse ?? notImplementedResponse() + } + + func submitPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return submitPasswordResponse ?? notImplementedResponse() + } + + func submitNewPassword(_ password: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return submitNewPasswordResponse ?? notImplementedResponse() + } + + func submitAttributes(_ attributes: [String: Any], state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return submitAttributesResponse ?? notImplementedResponse() + } + + func selectAuthMethod( + _ method: MSALAuthMethod, + verificationContact: String?, + state: MSALNativeAuthFlowInternalState + ) async -> MSALNativeAuthFlowControllerResponse { + return selectAuthMethodResponse ?? notImplementedResponse() + } + + func submitChallenge(_ challenge: String, state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + return submitChallengeResponse ?? notImplementedResponse() + } + + func resendCode(state: MSALNativeAuthFlowInternalState) async -> MSALNativeAuthFlowControllerResponse { + 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..d2d0b39323 --- /dev/null +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2RequestProviderMock.swift @@ -0,0 +1,153 @@ +// +// 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 tokenScopes: [String]? + 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(apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + authorizeChallengeStartCalled = true + return try resolveRequest() + } + + func authorizeChallengeContinue( + continuationToken: String, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + authorizeChallengeContinueCalled = true + return try resolveRequest() + } + + func token(code: String, scopes: [String], apiId: MSALNativeAuthTelemetryApiId, context: MSALNativeAuthRequestContext) throws -> MSIDHttpRequest { + tokenCalled = true + tokenScopes = scopes + 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, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + resetPasswordStartCalled = true + return try resolveRequest() + } + + 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, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + verifyCalled = true + verifyHrefReceived = href + return try resolveRequest() + } + + 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, + apiId: MSALNativeAuthTelemetryApiId, + context: MSALNativeAuthRequestContext + ) throws -> MSIDHttpRequest { + pollCalled = true + pollHrefReceived = href + return try resolveRequest() + } +} diff --git a/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseParserMock.swift b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseParserMock.swift new file mode 100644 index 0000000000..c5543112f0 --- /dev/null +++ b/MSAL/test/unit/native_auth/mock/v2/MSALNativeAuthV2ResponseParserMock.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 +@testable import MSAL +@_implementationOnly import MSAL_Private + +class MSALNativeAuthV2ResponseParserMock: MSALNativeAuthV2ResponseParsing { + + var authorizeChallengeResponses: [MSALNativeAuthV2AuthorizeChallengeParsedResponse] = [] + var interactionResponses: [MSALNativeAuthV2InteractionParsedResponse] = [] + + private(set) var parseAuthorizeChallengeCallCount = 0 + private(set) var parseInteractionCallCount = 0 + + func parseAuthorizeChallenge( + context: MSIDRequestContext, + _ result: Result, + flowScenario: MSALNativeAuthFlowScenario + ) -> MSALNativeAuthV2AuthorizeChallengeParsedResponse { + defer { parseAuthorizeChallengeCallCount += 1 } + if parseAuthorizeChallengeCallCount < authorizeChallengeResponses.count { + return authorizeChallengeResponses[parseAuthorizeChallengeCallCount] + } + return .error(MSALNativeAuthFlowError(type: .generalError)) + } + + func parseInteraction( + context: MSIDRequestContext, + _ result: Result + ) -> 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/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") + } } 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..35b3f2ccdf --- /dev/null +++ b/MSAL/test/unit/native_auth/network/responses/v2/MSALNativeAuthV2HALResponseSerializerTests.swift @@ -0,0 +1,222 @@ +// +// 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: - Concrete response types + + 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", + "type": "email" + ] + + let response = try parse(json, statusCode: 200) + let codeSent = try XCTUnwrap(response as? MSALNativeAuthHALCodeSentResponse) + + 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 { + 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] = [ + "action": "challenge", + "_embedded": [ + "methods": [ + [ + "id": "1", + "type": "email", + "hint": "u***@contoso.com", + "_links": ["challenge": ["href": "https://contoso.com/challenge"]] + ] + ] + ] + ] + + let response = try parse(json, statusCode: 200) + let challenge = try XCTUnwrap(response as? MSALNativeAuthHALChallengeResponse) + + 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: - 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) + } + + 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 { + 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.continuationToken) + XCTAssertNil(response.error) + XCTAssertTrue(response.links.isEmpty) + XCTAssertFalse(response.isWebFallbackRequired) + } + + 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..3c7d33e02d --- /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) + XCTAssertTrue(receivedResponse is MSALNativeAuthHALReadyToCompleteResponse) + } + + 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/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..d142903dac --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ParametersTests.swift @@ -0,0 +1,187 @@ +// +// 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/resetPassword" + let sut = MSALNativeAuthV2EntryParameters( + context: context, + target: .href(href), + apiId: .telemetryApiIdV2ResetPasswordStart, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.rawValue, + username: "user@contoso.com", + continuationToken: "CT" + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordStart) + XCTAssertEqual(sut.operationType, MSALNativeAuthV2OperationType.resetPasswordStart.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: .telemetryApiIdV2ResetPasswordStart, + operationType: MSALNativeAuthV2OperationType.resetPasswordStart.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: .telemetryApiIdV2ResetPasswordSubmitCode, + operationType: MSALNativeAuthV2OperationType.verify.rawValue, + requestBody: MSALNativeAuthV2VerifyRequestBody(continuationToken: "CT", otp: "1234") + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmitCode) + 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: MSALNativeAuthV2UpdatePasswordRequestBody(continuationToken: "CT", newPassword: "newPass") + ) + + XCTAssertEqual(sut.httpMethod, "PUT") + XCTAssertEqual(sut.body as? [String: String], ["continuationToken": "CT", "newPassword": "newPass"]) + } + + // MARK: - AuthorizeChallengeStartParameters + + func test_authorizeChallengeStartParameters_body_url_andMetadata() throws { + let sut = MSALNativeAuthV2AuthorizeChallengeStartParameters( + context: context, + clientId: "client-id", + apiId: .telemetryApiIdV2ResetPasswordStart + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordStart) + 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: .telemetryApiIdV2ResetPasswordSubmit + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmit) + 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: .telemetryApiIdV2ResetPasswordSubmit + ) + + XCTAssertEqual(sut.apiId, .telemetryApiIdV2ResetPasswordSubmit) + 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: .telemetryApiIdV2ResetPasswordSubmit + ) + + 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..e0e90940d8 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2RequestProviderTests.swift @@ -0,0 +1,157 @@ +// +// 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_resetPasswordStart_threadsApiId() throws { + let request = try sut.resetPasswordStart( + username: "user@contoso.com", + continuationToken: "CT", + href: href, + apiId: .telemetryApiIdV2ResetPasswordStart, + 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_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: .telemetryApiIdV2ResetPasswordSubmitCode, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(forHref: href)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmitCode) + } + + 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: .telemetryApiIdV2ResetPasswordStart, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .authorizeChallenge)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordStart) + } + + func test_authorizeChallengeContinue_usesAuthorizeChallengeEndpointAndThreadsApiId() throws { + let request = try sut.authorizeChallengeContinue( + continuationToken: "CT", + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) + + XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .authorizeChallenge)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmit) + } + + func test_token_usesTokenEndpointAndKeepsRawJSONSerializer() throws { + let request = try sut.token( + code: "auth-code", + scopes: ["scope1"], + apiId: .telemetryApiIdV2ResetPasswordSubmit, + context: context + ) + + XCTAssertEqual(request.urlRequest?.httpMethod, "POST") + XCTAssertEqual(request.urlRequest?.url, try resolver.url(for: .token)) + XCTAssertEqual(apiId(of: request), .telemetryApiIdV2ResetPasswordSubmit) + XCTAssertFalse(request.responseSerializer is MSALNativeAuthV2HALResponseSerializer) + } +} diff --git a/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift new file mode 100644 index 0000000000..7bd203a801 --- /dev/null +++ b/MSAL/test/unit/native_auth/network/v2/MSALNativeAuthV2ResponseParserTests.swift @@ -0,0 +1,306 @@ +// +// 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 MSALNativeAuthV2ResponseParserTests: XCTestCase { + + private var sut: MSALNativeAuthV2ResponseParser! + private var context: MSALNativeAuthRequestContext! + + override func setUp() { + super.setUp() + sut = MSALNativeAuthV2ResponseParser() + context = MSALNativeAuthRequestContextMock() + } + + // MARK: - Builders + + private func makeResponse( + statusCode: Int = 200, + state: String? = nil, + action: String? = nil, + continuationToken: String? = nil, + codeLength: Int? = nil, + hint: String? = nil, + methodType: String? = nil, + code: String? = nil, + links: [String: String] = [:], + 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 + ) + } + + 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" { + return MSALNativeAuthHALReadyToCompleteResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + } + + return MSALNativeAuthHALResponse( + statusCode: statusCode, + correlationId: nil, + continuationToken: continuationToken, + links: links, + error: error, + isWebFallbackRequired: isWebFallbackRequired + ) + } + + // MARK: - parseAuthorizeChallenge + + func test_parseAuthorizeChallenge_withContinuationToken() { + let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) + let result = sut.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .passwordReset) + XCTAssertEqual(result, .continuationToken(continuationToken: "ct", href: "https://contoso.com/reset")) + } + + func test_parseAuthorizeChallenge_missingFlowLink_returnsError() { + let response = makeResponse(statusCode: 401, continuationToken: "ct", links: ["reset_password": "https://contoso.com/reset"]) + 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_parseAuthorizeChallenge_withAuthorizationCode() { + let response = makeResponse(code: "auth-code") + let result = sut.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) + XCTAssertEqual(result, .authorizationCode(code: "auth-code")) + } + + 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.parseAuthorizeChallenge(context: context, .success(response), flowScenario: .signIn) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) + } + + 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: - parseInteraction + + func test_parseInteraction_challengeAction_returnsChallengeRequired() { + 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")) + } + + func test_parseInteraction_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.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_parseInteraction_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.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_parseInteraction_updateAction_returnsUpdateRequired() { + let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct", links: ["update": "https://contoso.com/update"]) + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .updateRequired(continuationToken: "ct", updateHref: "https://contoso.com/update")) + } + + func test_parseInteraction_pollAction_returnsPollInProgress() { + let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct", links: ["poll": "https://contoso.com/poll"]) + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .pollInProgress(continuationToken: "ct", pollHref: "https://contoso.com/poll")) + } + + func test_parseInteraction_updateAction_withoutUpdateLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "update", continuationToken: "ct") + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) + } + + func test_parseInteraction_pollAction_withoutPollLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "poll", continuationToken: "ct") + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) + } + + func test_parseInteraction_verifyAction_withoutVerifyLink_failsWithMissingLink() { + let response = makeResponse(state: "interactionRequired", action: "verify", continuationToken: "ct", codeLength: 8, hint: "u***@contoso.com") + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) + } + + func test_parseInteraction_continueState_returnsReadyToComplete() { + let response = makeResponse(state: "continue", continuationToken: "ct") + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .readyToComplete(continuationToken: "ct")) + } + + func test_parseInteraction_webFallbackRequiredState_returnsBrowserRequired() { + let response = makeResponse( + state: "webFallbackRequired", + continuationToken: "ct", + links: ["webFallback": "https://contoso.com/oauth2/v2.0/authorize"] + ) + let result = sut.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .browserRequired) + } + + 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.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .browserRequired) + } + + 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.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .userNotFound))) + } + + 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.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCode))) + } + + 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.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .generalError))) + } + + 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.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidPassword))) + } + + 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.parseInteraction(context: context, .success(response)) + XCTAssertEqual(result, .error(MSALNativeAuthFlowError(type: .invalidCredentials))) + } +} 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) + } + } +}