Implement V2 (HAL-based) SSPR flow for native auth - #3057
Conversation
Introduce a unified, server-driven (HAL-JSON) Native Auth V2 surface that mirrors V1's internal architecture (controller / request-provider / validator / state / delegate / factory) while reusing the shared MSIDHALResource/MSIDHALLink parsing types from IdentityCore. - Public surface: MSALNativeAuthFlowError/Action/Delegate/FlowState. - Unified controller drives the full SSPR sequence (authorize-challenge -> resetpassword start -> challenge -> verify -> update -> poll -> authorize-challenge -> token). signUp/signIn V2 return notImplemented. - Network layer builds MSIDHttpRequest directly; HAL serializer + error handler funnel any HTTP status (incl. the bootstrap 401) into the validator. - Public app gains resetPasswordV2/signUpV2/signInV2; V1 signUp/signIn/ resetPassword are deprecated in favour of the V2 entry points. - Unit tests + mocks for the V2 controller and response validator. - Bump IdentityCore submodule to include the HAL serializer types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dc issues url encoding
generic swiftlint rules for AI
…ering, comment cleanup
Fail fast on missing bootstrap links, make provider hrefs non-optional,
consistently order request-provider methods, and strip step-numbered /
V1-comparison narration comments.
- Controller entry points (signUp/signIn/resetPassword) now resolve their
required bootstrap link (sign_up/sign_in/reset_password) right after the
authorize-challenge guard and fail early via missingLinkFailure(...) with an
"improper response" error, instead of passing an optional href that only
fails on the next request. Renamed the local bootstrapToken -> continuationToken.
- MSALNativeAuthV2RequestProviding: signUpStart/signInStart/resetPasswordStart
now take a non-optional href (always .href(href)); resetPasswordStart uses its
bootstrap reset_password link. Removed the now-dead signInStart/signUpStart/
resetPasswordStart cases from MSALNativeAuthV2Endpoint.
- Reordered all V2 request-provider methods (protocol + impl) to
signUp -> signIn -> resetPassword -> HAL methods -> authorize-challenge
(start/continue) -> token (last). Paren-aligned multi-parameter signatures.
- Removed step-numbered ("Step 1 - ...", "(step 7)") and V1-comparison
comments across the V2 layer; comments now describe what each piece does
without implying a fixed call order.
- Updated the request-provider mock and one reset-password test to match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…2 validator Move required-link validation into the V2 response validator so a flow fails as soon as the server response is missing the link the next step needs, instead of failing later on a subsequent request. - Thread MSALNativeAuthV2FlowType from each entry point (signUp/signIn/ resetPassword) down to validateAuthorizeChallenge. Each flow now declares its flowType exactly once; downstream steps derive it from the flow type or the continuation state. - Resolve the single flow-specific entry link in the validator: .continuationToken now carries a non-optional href instead of a links dictionary, and a missing link returns a descriptive .error immediately. - Make interaction-response hrefs non-optional and guard each required link in validateInteraction, so the controller can no longer receive a nil href. - Expose MSALNativeAuthV2FlowType.link (CaseIterable) as the single source of the sign_up/sign_in/reset_password relations; drop the duplicated serializer list. - Remove dead concatenated link spellings (signup/signin/resetpassword/ submitattributes); sign_up/sign_in/reset_password are the only valid relations. - Rename the authorize-challenge step off "bootstrap" for clarity. - Update the validator mock and add fail-fast regression tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…clarity - Rename ambiguous token2 to challengeContinuationToken in the sign-in and reset-password start paths. - Thread the authorization-challenge hint through sign-in so codeRequired and activationRequired fall back to the masked destination when the challenge response omits sentTo, matching the reset-password behavior. - Split selectAuthMethod into explicit JIT (enroll) vs MFA (challenge) branches, each owning its own guard, error message, and telemetry id so a missing link is reported against the correct flow. - Split submitChallenge into JIT (activate) vs MFA (verify) so a missing link reports exactly which link was absent instead of "verify/activate". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…kenCacher Both native-auth token controllers now delegate token-response caching to a single MSALNativeAuthTokenCacher helper (clear existing account, save tokens, validate account). Account validation is injected as a closure so each controller keeps using its own validator, restoring the post-save account validation that was missing on the server-driven flow path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Share token-request handling between the token controller and the V2 flow controller via a MSALNativeAuthTokenRequestHandling protocol with a default implementation, keeping performTokenRequest off the base controller. Extract the request interceptor bridge into a single shared type used by both the V1 and V2 request configurators. Route V2 response errors through isError so the server error is surfaced up the chain in all validator paths, and drop unused HALResource link accessors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the isError computed property and bind response.error directly in the V2 response validator, dropping the extra flowError overload and its defensive nil branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Auto-submit the email/password (and any pre-supplied attributes) the app
provides at sign-up start when the server issues collectAttributes for them,
keeping the values private (internal continuation state, never surfaced to the
app, never logged). The full value set is retained for the whole flow; only the
attributes the server requests in each step are sent.
Track auto-submitted attribute ids and, if the server re-requests an attribute
already submitted (e.g. after a validation failure), surface a general error to
the app instead of resending in a loop. The error names the attribute id only,
never the value.
Also fix the /auth/methods/{type}/{id}/verify endpoint to send the code in the
`otp` field (not `code`) for sign-in and sign-up, matching the server contract
(previously returned AADSTS90023). MFA verify uses `verify`/`otp`; the JIT
`activate` link keeps `submitCode`/`code`.
Adds regression tests for the autofill happy path, the repeat-attribute error,
and the otp verify field for sign-in/sign-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e612cb1a-744b-4c23-8156-b1ac27d3ccad
…tive-auth-v2 Link controller to public api
Replace scattered string literals in the Native Auth V2 (HAL) request/response paths with pure String-backed enums, mirroring V1's MSALNativeAuthRequestParametersKey style (cases only, no static helpers). Wire format is preserved byte-for-byte. - Add MSALNativeAuthV2RequestBodyKey (camelCase HAL body keys) - Add MSALNativeAuthV2LinkRelation (HAL _links relation names) - Add MSALNativeAuthV2HALAction (the response `action` verb) - Route request bodies, link lookups, and the validator switch through the enums - Split per-method links into a typed methodLinks collection on the continuation state (drop the composite "method:<id>" key) - Trim comments that merely restate the code - Fix pre-existing test-target breaks (mock param types, flow scenario case) - Add build/test guidelines to .clinerules/AGENTS.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2
Replace the hand-assembled [AnyHashable: Any] body dictionaries in the V2 HAL request provider with a typed MSALNativeAuthV2RequestBody. Each field maps to a MSALNativeAuthV2RequestBodyKey and is emitted only when set, so call sites state just the fields an operation carries (e.g. a password submit sets `password`, a poll sets nothing extra). The generated body dictionary is unchanged, so the wire format is preserved. - Add MSALNativeAuthV2RequestBody (typed continuationToken + optional fields) - MSALNativeAuthV2HrefParameters now carries the typed body and derives `body` - Route the 8 HAL provider methods through the typed body - Register the new file in project.pbxproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2
…(V1-style) Thread MSIDRequestContext through the V2 response validator's three validate methods (mirroring V1 validators) and emit decision/error log statements. Server error messages and transport failures are masked via MSALLogMask.maskPII; no tokens, continuation tokens, or hints are logged. Updates the flow controller call sites, the validator mock, and tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1950ebbe-35fe-4325-861d-95117a60f4c2
The V2 'validator' returns a response model rather than a valid/invalid result, so 'parser' better reflects its behavior. Renames the protocol, class, validated-response enums, methods, files and enclosing group from validator/Validated to parser/Parsed. V1 validators and account validation are unaffected. Also fixes a latent context: context() call in the browserRequired mapInteraction test that had never been compiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecf13a9d-6bea-4c2f-834e-ef32ed967500
| 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") { |
There was a problem hiding this comment.
Since v2 is server-driven and we POST the continuation token — and for verify/updatePassword the OTP and the new password — to whatever _links href comes back, what constrains the host we send those to? In the absolute-href branch we take the server URL as-is (scheme.hasPrefix("http")) with no check that its host matches authorityURL. Two things worth confirming: (1) should we validate the absolute href's host against the configured authority host before following it, so a tampered/misconfigured response can't redirect credentials to another host? and (2) hasPrefix("http") also accepts http:// — should we require https here so we never downgrade a code/password to cleartext? I get that today DC returns hrefs on the same host and TLS protects transit, but for a credential-bearing flow the host allow-list feels like cheap defense-in-depth. (Note: the relative branch already reconstructs against the authority, so this is only the absolute path.)
There was a problem hiding this comment.
Thanks for the feedback, we are trying to get the API team to return just the API endpoint, like oauth2/v2.0/authorize-challenge and then we would be able to use the v1 URL Resolver so this class would go away. Added a //TODO a the start of the class to change it once we clarify conversation
| ) | ||
| } | ||
|
|
||
| guard case .pollInProgress(var pollToken, let pollHref) = updateResult else { |
There was a problem hiding this comment.
This step guards only on .pollInProgress and sends everything else to interactionFailure(..., newState: nil). But a weak-password rejection at update maps to .invalidPassword (passwordTooWeak), and here that would come back with newState: nil — so the user can't retry the password and has to restart the whole reset. mapInteraction deliberately keeps invalidPassword/invalidCode recoverable (and you have test_mapInteraction_error_invalidPassword_isRecoverable for it), but submitNewPassword bypasses mapInteraction, so that recoverability is lost on this path. Could we route the update result through the same recoverable-state handling (or at least pass recoverableState: state for .invalidPassword)? Relatedly, if update ever returns .readyToComplete/.updateRequired directly instead of a poll link, we'd also treat it as a generic failure — is polling always guaranteed by the API here?
There was a problem hiding this comment.
Good catch re the unrecoverable state, update to first check if we have any error and fail based on type
The API contract does say that update must return poll after
Convert MSALNativeAuthV2LinkRelation from an enum to a RawRepresentable struct with one constant per extension, so the type stays open for extension without core edits. Remove the enroll, register, activate, and submitAttributes relations that never appear in the observed responses. Update the flow controller tests to key links by typed relation constants instead of raw strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecf13a9d-6bea-4c2f-834e-ef32ed967500
fb4abe2 to
9edc2d3
Compare
fixed endpoints
| 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] |
There was a problem hiding this comment.
should we have a new callback (state) to request scopes just before we call /token enpoint? This will also allow us to keep same SSPR params in V2 as we had in V1
| let flowScenario: MSALNativeAuthFlowScenario | ||
| let continuationToken: String? | ||
| let links: [MSALNativeAuthV2LinkKey: URL] | ||
| let username: String? |
There was a problem hiding this comment.
when do we need it? Also in what flows it is needed?
Overview
This PR implements the V2 (server-driven / HAL-based) self-service password reset (SSPR) flow for native auth on iOS and macOS.
Unlike the V1 client-driven flow, V2 is driven by the server's HAL responses: the client follows
_linksrelations returned in each response to decide the next network call, rather than hard-coding the sequence of endpoints.What's included
Flow orchestration
MSALNativeAuthFlowController— orchestrates the SSPR flow (start → challenge → verify code → update password → completion) by mapping validated responses onto the next required state.MSALNativeAuthFlowResponseDispatcher— routes validated responses to the appropriate public state / delegate.MSALNativeAuthFlowContinuationState,MSALNativeAuthFlowInternalState,MSALNativeAuthFlowControllerResponse,MSALNativeAuthFlowResult— internal state plumbing carrying continuation tokens and resolved HAL links between layers.MSALNativeAuthFlowControlling— controller protocol seam.HAL networking
HALResource,MSALNativeAuthHALResponse,MSALNativeAuthV2HALAction,MSALNativeAuthV2HALResponseSerializer— HAL response model and serialization.MSALNativeAuthV2ResponseValidator/MSALNativeAuthV2ValidatedResponses— validate HAL responses into typed, actionable outcomes (code required, update required, poll in progress, ready to complete, error).MSALNativeAuthV2ResponseErrorHandler,MSALNativeAuthV2HrefURLResolver,MSALNativeAuthV2LinkRelation,MSALNativeAuthV2Endpoint— error handling and_linksresolution.Request building (parameters-driven, mirroring V1)
MSALNativeAuthV2RequestProvider/MSALNativeAuthV2RequestConfigurator— build requests from typed parameter objects.MSALNativeAuthV2EntryParameters,MSALNativeAuthV2HrefParameters,MSALNativeAuthV2AuthorizeChallengeStart/ContinueParameters,MSALNativeAuthV2TokenParameters,MSALNativeAuthV2RequestTarget,MSALNativeAuthV2Requestable.MSALNativeAuthV2RequestBody/MSALNativeAuthV2RequestBodyKey— typed request body.Token handling & telemetry
MSALNativeAuthTokenCacher,MSALNativeAuthTokenRequestHandling— extracted token request/caching seam reused by the V2 completion step.Tests
Notes