Skip to content

Native Auth V2: Sign-up - #3240

Open
Silviu Petrescu (spetrescu84) wants to merge 39 commits into
devfrom
spetrescu/native-auth-v2-signup
Open

Native Auth V2: Sign-up#3240
Silviu Petrescu (spetrescu84) wants to merge 39 commits into
devfrom
spetrescu/native-auth-v2-signup

Conversation

@spetrescu84

Copy link
Copy Markdown
Contributor

Summary

Adds the common / common4j layer for Native Auth V2 sign-up, alongside the existing V2 sign-in and SSPR support.

Mirrors the iOS V2 sign-up scenarios (AzureAD/microsoft-authentication-library-for-objc#3093) and follows the established V2 sign-in / SSPR code shape:

  • common4j: sign-up request provider, response parser (collectAttributes → AttributesRequired, userAlreadyExists, attributeValidationError → InvalidAttributes), interactor (performSignUpStart / performSubmitAttributes), and continuation-state submittedAttributes inheritance.
  • common: NativeAuthV2FlowController sign-up orchestration; shared submitCode ReadyToComplete → SignInAfterSignUpRequired.

Tests

  • New: SignUp response parser, request provider, interactor, sign-up flow controller, continuation-state tests.
  • Full V2 controller/parser/provider/interactor suites green; remaining full-suite failures are pre-existing network/env only.

Draft — depends on the paired msal PR.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Extends the V2 SSPR foundation with the Common-side pieces of Native Auth V2
sign-in, covering username + password as the first factor and password followed
by an email one-time-code second factor.

common4j:
- Parse challengeContext.authenticationFactor so a challenge can be classified
  as single- or multi-factor, and surface every server-offered method as a new
  opaque-href-free NativeAuthV2AuthMethod.
- Retain each method's own links inside NativeAuthV2ContinuationState and follow
  them via withSelectedMethod(); a stale or unknown method ID fails rather than
  falling back to another method's href. No endpoint path is ever built from a
  method ID.
- Add sign-in operations, results (MFARequired, PasswordRequired,
  InvalidCredentials, AuthMethodBlocked) and operation-scoped error mapping for
  user-not-found, invalid credentials, invalid one-time code and blocked method.
  The SSPR email-only policy and its error mappings are untouched.
- Add the sign-in entry and password verify requests, plus interactor operations
  that clear the password buffer in a finally on every exit path.

common:
- Add sign-in start, submit-password, select-MFA-method and submit-MFA-challenge
  commands and controller orchestration. Password is the only supported first
  factor; an offer without it fails deterministically instead of falling back to
  an email one-time code first factor.
- Preserve password-submission context so MSAL can report an entry-supplied
  failure as invalid credentials and a deferred one as an invalid password.
- Share one terminal path with SSPR for authorize-challenge continue, the
  authorization-code token exchange and cache persistence, so scopes, claims and
  correlation ID behave identically in both flows.
- Add sign-in specific PublicApiIds rather than reusing the SSPR ones.

Account-cache conflict enforcement stays out of Common; MSAL keeps the Android
V1 behaviour of rejecting sign-in while an account is signed in.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
common4j:spotbugsMain failed with two EI_EXPOSE_REP2 violations, one on each
Lombok-generated builder that takes the mutable char[] password field:
SignInV2StartCommandParametersBuilder and
NativeAuthV2SubmitPasswordCommandParametersBuilder.

Apply the same suppression the existing password-carrying parameter classes
already use, including SignInStartCommandParameters,
SignInSubmitPasswordCommandParameters and
NativeAuthV2SubmitNewPasswordCommandParameters. No other production change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep the prior private constructor shape so current MSAL consumers that create the opaque state reflectively continue to work while method-specific links default to empty.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

❌ Work item link check failed. Description does not contain AB#{ID}.

Click here to Learn more.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use continuation-state scopes and claims for deferred password and MFA token completion, and cover MFA method links across serialization.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…-auth-v2-signup

# Conflicts:
#	common4j/src/main/com/microsoft/identity/common/java/eststelemetry/PublicApiId.java
@spetrescu84 Silviu Petrescu (spetrescu84) changed the title Native Auth V2: Sign-up (common) Native Auth V2: Sign-up Sep 2, 2026
…internal/controllers/v2/NativeAuthV2FlowController.kt

Co-authored-by: Silviu Petrescu <111577419+spetrescu84@users.noreply.github.com>
@antrix1989

Copy link
Copy Markdown

Cross-platform review: CIAM Native Auth V2 sign-up (Android common ↔ MSAL iOS dev)

Reviewed this PR against the merged iOS counterpart (AzureAD/microsoft-authentication-library-for-objc#3093, now in dev). Overall the port is faithful — the HAL action names, the "entry POST deliberately omits the username so the server asks for collectAttributes" trick, the upfront email + password + app-attributes submit, the case-insensitive submittedAttributes bookkeeping, and routing the sign-up password through submitAttributes (not the sign-in verify endpoint) all match iOS exactly.

The findings below are the places where the two SDKs will behave differently, plus one Android-only defect.


1. Severity: High — a sign-up that completes on the upfront attribute submit returns a generic error

Issue. NativeAuthV2CommandResult.SignInAfterSignUpRequired implements NativeAuthV2SubmitAttributesCommandResult and NativeAuthV2SubmitCodeCommandResult, but not NativeAuthV2SignUpStartCommandResult.

signUpStart can legitimately produce it. The path is:

signUpStart
  → performSignUpStart                     → AttributesRequired
  → handleSignUpAttributesRequired(upfront) → performSignUpSubmitAttributes
  → handleSignUpInteractionResult(ReadyToComplete) → SignInAfterSignUpRequired
  → signUpStart: result as? NativeAuthV2SignUpStartCommandResult   ← null
  → unexpectedSignUpApiError(...)

Impact. For a tenant configured for password sign-up without email OTP verification — i.e. the server returns state: "continue" straight off the upfront submitAttributes — the account is created server-side, but the app receives a generic APIError ("Unexpected sign-up result") and has no SignInAfterSignUpStateV2 to continue from. The user is stranded with an account they can't sign into via the returned state.

This cannot happen on iOS: MSALNativeAuthFlowController.signUp and handleSignUpInteractionResult share one MSALNativeAuthSignUpControllerResponse enum, whose .readyToComplete case is reachable from the start step. Android's per-operation sealed marker interfaces reintroduce the gap.

NativeAuthV2SignUpFlowControllerTest covers testSubmitAttributesCompletesWithSignInAfterSignUpRequired but has no equivalent for the start step, so this isn't caught.

Recommendation. Add NativeAuthV2SignUpStartCommandResult to SignInAfterSignUpRequired's supertype list, map it in NativeAuthPublicClientApplication.signUpV2 (msal#2565), and add a testSignUpStartCompletesWithSignInAfterSignUpRequired case.


2. Severity: HighuserAlreadyExists is read from a different wire location than iOS

Issue. The two SDKs disagree about where the server puts this error code.

Location matched
Android (mapInteractionError) error.innerError.details[].code == "userAlreadyExists"
iOS (MSALNativeAuthV2ResponseParser.flowError(from:)) top-level error.code == "userAlreadyExists"

iOS's MSALNativeAuthV2HALResponseSerializer.parseError doesn't parse innerError.details at all, so it structurally cannot see the shape your test fixture asserts:

"error": { "code": "invalidRequest", "innerError": { "details": [
  { "attributeIds": ["email"], "code": "userAlreadyExists", "message": "..." } ] } }

Impact. At most one of these is correct against the real CIAM v2 HAL contract. Whichever is wrong silently degrades userAlreadyExists to a generic error — on iOS today, .generalError; on Android, APIError. Apps then can't offer "you already have an account, sign in instead."

Recommendation. Confirm the wire shape with the ESTS/CIAM v2 contract owners and align both SDKs. If the server can emit either shape, match both (top-level code OR any innerError.details[].code) and file the corresponding iOS work item. Please link the contract reference in the PR description so reviewers on both platforms can verify.


3. Severity: MediumattributeValidationErrorAttributesInvalid has no iOS counterpart

Issue. INNER_ERROR_ATTRIBUTE_VALIDATION_FAILED = "attributeValidationError" (matched on error.innerError.code) produces NativeAuthV2InteractionApiResult.InvalidAttributes → the public NativeAuthResultV2.AttributesInvalid with a retryable AttributesInvalidStateV2.

The iOS V2 parser has no mapping for attributeValidationError. MSALNativeAuthAttributesInvalidState exists as a class, but nothing in the V2 code path constructs it — the attributeValidationFailed handling on iOS is V1-only (network/responses/validator/sign_up/).

Impact. The same server response (e.g. a password-policy violation on submit) is an actionable, retryable state on Android and an opaque generalError on iOS. This is the largest public-contract divergence in the PR, and it's in the good direction — but it means "mirrors the iOS V2 sign-up scenarios" isn't accurate for this branch.

Recommendation. Keep the Android behaviour and file the iOS gap, or hold this mapping back until iOS lands it. Either way, call it out explicitly in the PR description so the platforms don't quietly diverge.


4. Severity: MediumparseCollectAttributes has no self fallback for the submitAttributes link

Issue. iOS resolves the submit target defensively:

collectAttributesResponse.href(for: .submitAttributes) ?? collectAttributesResponse.href(for: .self)

(the same ?? .self fallback it uses for .update). Android returns missingLinkError(...) the moment links["submitAttributes"] is absent. Compounding it, self isn't in NativeAuthV2ContinuationState.SUPPORTED_RELATIONS, so a self href would be dropped from the continuation state even if the response carried one.

Impact. A collectAttributes body that only self-links — the exact shape iOS defends against — works on iOS and hard-fails mid-flow on Android.

Recommendation. Mirror the iOS fallback and add self to SUPPORTED_RELATIONS. If the fallback is deliberately omitted because the contract guarantees submitAttributes, please note that in the KDoc and file the iOS side as dead code.


5. Severity: Medium — the password wipe doesn't do what the docs claim

Issue. NativeAuthV2FlowController.upfrontAttributeValues:

values[ATTRIBUTE_NAME_PASSWORD] = String(password)

That materializes an immutable heap String, which then flows into NativeAuthV2SubmitAttributesRequestParameters.attributes (Map<String, String>), through the request body, and is never cleared. The finally { StringUtil.overwriteWithNull(parameters.password) } in signUpStart only zeroes the char[].

SignUpV2StartCommandParameters's Javadoc states "The password is never retained in continuation or public state; the controller clears the buffer once the request has been issued," and the inline comment says it's cleared "so it never outlives the request." Neither holds for the String copy — it lives until GC and can be captured in a heap dump.

Impact. Not a new leak relative to the rest of the SDK, but the documented security property is stronger than the implementation. That mismatch is the risk: a future reader will trust the comment.

Recommendation. Either (a) thread the password to the JSON body as char[]/CharSequence so no immutable copy is created, or (b) correct the Javadoc and inline comments to describe what actually happens. The same pattern appears in msal#2565 PasswordRequiredStateV2.submitSignUpPassword (mapOf("password" to String(passwordCopy))) with an equally strong comment.


6. Severity: Medium — attribute values are Map<String, String>; iOS uses [String: Any]

Issue. NativeAuthV2SubmitAttributesRequest, createSubmitAttributesRequest, performSubmitAttributes, SignUpV2StartCommandParameters.attributes, and NativeAuthV2SubmitAttributesCommandParameters.attributes are all Map<String, String>. iOS's MSALNativeAuthV2SubmitAttributesRequestBody.attributes is [String: Any], guarded by JSONSerialization.isValidJSONObject, so numeric/boolean CIAM extension attributes serialize with their native JSON type.

Impact. An extension_age: 30 attribute goes out as "30" from Android and 30 from iOS. If the directory schema types the attribute as a number, one of those is rejected.

Assumption: this may be a deliberate Android-wide constraint — V1 UserAttributes is also string-only — in which case disregard. But please confirm the v2 submitAttributes contract coerces stringified values for non-string attribute types; if it doesn't, this is a functional gap rather than a style difference.


7. Severity: Medium — no email-channel guard on the sign-up CodeRequired branch

Issue. iOS's handleSignUpInteractionResult guards the .verificationRequired case:

guard challenge.channelType.isEmailType else { /* general error:
    "Sign up currently supports email one-time-code verification only" */ }

Android's CodeRequired branch in handleSignUpInteractionResult surfaces whatever challengeChannel the server sent, with no check.

Impact. If a tenant is configured with SMS OTP for sign-up, Android hands the app a CodeRequiredStateV2 for a channel the flow was never validated against, while iOS fails fast with a clear message. Divergent and harder to diagnose.

Recommendation. Add the same guard, or document why Android intentionally accepts all channels.


8. Severity: MediumAttributesInvalid.invalidAttributes flattens unrelated detail entries

Issue.

serverError.details.flatMap { it.attributeIds }.distinct()

details is the full innerError.details array. Entries whose code is unrelated to the validation failure (e.g. a userAlreadyExists detail carried alongside, which your own fixture comment anticipates) contribute their attributeIds too.

Impact. The app can be told to correct an attribute the server didn't actually reject.

Recommendation. Filter to details that belong to the validation failure before flattening, or document that invalidAttributes is a superset.


9. Severity: Medium — upfront password / attributes are silently discarded unless the first response is collectAttributes

Issue. signUpStart passes upfront = parameters into handleSignUpInteractionResult, but every branch except AttributesRequired ignores it. If the server responds to the entry POST with verify or readyToComplete, the caller's password and attributes are dropped with no signal — the app then gets CodeRequired, and the password it supplied to signUpV2() never reaches the server.

iOS has the identical shape, so this is a shared design gap, not an Android regression. Flagging it because the "entry POST omits the username so we always get collectAttributes back" assumption is load-bearing and undefended on both platforms.

Recommendation. At minimum a Logger.warn when upfront is non-null and gets dropped, so the failure is diagnosable from logs.


10. Severity: MediumretryState for AttributesInvalid is the pre-submit state

Issue. performSignUpSubmitAttributes passes retryState = state — the state before withAdditionalSubmittedAttributes(...) is applied.

Impact. After a rejected upfront submit, the app retries from a state whose submittedAttributes is empty. If the server subsequently re-requests email (already sent), the flow surfaces AttributesRequired to the app instead of the intended "already submitted" hard error, defeating the loop-detection this PR adds.

Recommendation. Use state.withAdditionalSubmittedAttributes(attributes.keys) as the retry state. The continuation token is unchanged either way, so this is a pure bookkeeping fix.


11. Non-blocking — NativeAuthV2RequiredAttribute.required nullability

iOS coerces a missing required to false (attribute["required"] as? Bool ?? false). Android carries Boolean? all the way to the public RequiredUserAttribute.required, so apps see null on Android where iOS sees false. RequiredUserAttribute is a pre-existing V1 type so changing it isn't in scope here, but the V2 NativeAuthV2RequiredAttribute could default it.


Cycle summary

  • New issues: 11 (2 High, 8 Medium, 1 non-blocking)
  • Resolved issues:
  • Remaining blockers: Update Readme #1 (SignInAfterSignUpRequired unreachable from signUpStart — account created, generic error returned) and Updated readme. closes #1 #2 (userAlreadyExists wire-location contradiction with iOS).

Verified as not issues, for the record: parcel read/write symmetry in the new V2 states; routing the sign-up password through submitAttributes rather than the sign-in submit-password endpoint; submittedAttributes case-insensitivity (lowercase() ≈ iOS caseInsensitiveCompare); HAL action string values; and the absence of reserved-name filtering in the deferred submitAttributes path (iOS behaves the same).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 27924655-bd09-44b3-823c-f8c5effaa35d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7f0a19d7-6a3a-4935-bf49-5e95967ad226
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ctor

- NativeAuthV2Interactor.executeJsonInteraction now takes an
  HttpClient.HttpMethod parameter defaulting to POST and dispatches through
  httpClient.method(...), with performUpdatePassword passing PUT, replacing
  the usePut boolean flag.
- Remove the unused private compatibility constructor from
  NativeAuthV2ContinuationState and the reflection-based test that was its
  only consumer.
- Update interactor test doubles to stub httpClient.method(...).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84d1b795-fc24-4f58-a26a-1630241c9c14
parseVerify handles every `verify` action, so a password-typed challenge was
turned into PasswordRequired regardless of which step produced it. Track the
server's challengeContext.authenticationFactor on NativeAuthV2ContinuationState,
inheriting it across responses that do not restate it, and return a dedicated
invalid-state error when a password challenge arrives on a step that is not the
first factor. An unclassified state fails closed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84d1b795-fc24-4f58-a26a-1630241c9c14
The user-not-found branch matched only on the AADSTS50034 substring in the
server message, with no constraint on the outer error code, and sat above the
invalid-credentials branch. A message that merely mentions AADSTS50034 while
reporting a recoverable credentials failure would therefore be reported as a
terminal UserNotFound instead of InvalidCredentials.

Gate the branch on the invalidRequest outer code and move it below the
credentials branch, matching the reference implementation in MSAL for objc.
The two branches are now mutually exclusive on the outer code.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 84d1b795-fc24-4f58-a26a-1630241c9c14
…-auth-v2-signup

# Conflicts:
#	common/src/main/java/com/microsoft/identity/common/nativeauth/internal/controllers/v2/NativeAuthV2FlowController.kt
#	common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/NativeAuthV2OAuth2Strategy.kt
#	common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/interactors/NativeAuthV2Interactor.kt
#	common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2ContinuationState.kt
#	common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2Operation.kt
#	common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2ResponseParser.kt
@spetrescu84
Silviu Petrescu (spetrescu84) marked this pull request as ready for review September 8, 2026 15:06
@spetrescu84

Copy link
Copy Markdown
Contributor Author

Cross-platform review: CIAM Native Auth V2 sign-up (Android common ↔ MSAL iOS dev)

Reviewed this PR against the merged iOS counterpart (AzureAD/microsoft-authentication-library-for-objc#3093, now in dev). Overall the port is faithful — the HAL action names, the "entry POST deliberately omits the username so the server asks for collectAttributes" trick, the upfront email + password + app-attributes submit, the case-insensitive submittedAttributes bookkeeping, and routing the sign-up password through submitAttributes (not the sign-in verify endpoint) all match iOS exactly.

The findings below are the places where the two SDKs will behave differently, plus one Android-only defect.

1. Severity: High — a sign-up that completes on the upfront attribute submit returns a generic error

Issue. NativeAuthV2CommandResult.SignInAfterSignUpRequired implements NativeAuthV2SubmitAttributesCommandResult and NativeAuthV2SubmitCodeCommandResult, but not NativeAuthV2SignUpStartCommandResult.

signUpStart can legitimately produce it. The path is:

signUpStart
  → performSignUpStart                     → AttributesRequired
  → handleSignUpAttributesRequired(upfront) → performSignUpSubmitAttributes
  → handleSignUpInteractionResult(ReadyToComplete) → SignInAfterSignUpRequired
  → signUpStart: result as? NativeAuthV2SignUpStartCommandResult   ← null
  → unexpectedSignUpApiError(...)

Impact. For a tenant configured for password sign-up without email OTP verification — i.e. the server returns state: "continue" straight off the upfront submitAttributes — the account is created server-side, but the app receives a generic APIError ("Unexpected sign-up result") and has no SignInAfterSignUpStateV2 to continue from. The user is stranded with an account they can't sign into via the returned state.

This cannot happen on iOS: MSALNativeAuthFlowController.signUp and handleSignUpInteractionResult share one MSALNativeAuthSignUpControllerResponse enum, whose .readyToComplete case is reachable from the start step. Android's per-operation sealed marker interfaces reintroduce the gap.

NativeAuthV2SignUpFlowControllerTest covers testSubmitAttributesCompletesWithSignInAfterSignUpRequired but has no equivalent for the start step, so this isn't caught.

Recommendation. Add NativeAuthV2SignUpStartCommandResult to SignInAfterSignUpRequired's supertype list, map it in NativeAuthPublicClientApplication.signUpV2 (msal#2565), and add a testSignUpStartCompletesWithSignInAfterSignUpRequired case.

2. Severity: HighuserAlreadyExists is read from a different wire location than iOS

Issue. The two SDKs disagree about where the server puts this error code.

Location matched
Android (mapInteractionError) error.innerError.details[].code == "userAlreadyExists"
iOS (MSALNativeAuthV2ResponseParser.flowError(from:)) top-level error.code == "userAlreadyExists"
iOS's MSALNativeAuthV2HALResponseSerializer.parseError doesn't parse innerError.details at all, so it structurally cannot see the shape your test fixture asserts:

"error": { "code": "invalidRequest", "innerError": { "details": [
  { "attributeIds": ["email"], "code": "userAlreadyExists", "message": "..." } ] } }

Impact. At most one of these is correct against the real CIAM v2 HAL contract. Whichever is wrong silently degrades userAlreadyExists to a generic error — on iOS today, .generalError; on Android, APIError. Apps then can't offer "you already have an account, sign in instead."

Recommendation. Confirm the wire shape with the ESTS/CIAM v2 contract owners and align both SDKs. If the server can emit either shape, match both (top-level code OR any innerError.details[].code) and file the corresponding iOS work item. Please link the contract reference in the PR description so reviewers on both platforms can verify.

3. Severity: MediumattributeValidationErrorAttributesInvalid has no iOS counterpart

Issue. INNER_ERROR_ATTRIBUTE_VALIDATION_FAILED = "attributeValidationError" (matched on error.innerError.code) produces NativeAuthV2InteractionApiResult.InvalidAttributes → the public NativeAuthResultV2.AttributesInvalid with a retryable AttributesInvalidStateV2.

The iOS V2 parser has no mapping for attributeValidationError. MSALNativeAuthAttributesInvalidState exists as a class, but nothing in the V2 code path constructs it — the attributeValidationFailed handling on iOS is V1-only (network/responses/validator/sign_up/).

Impact. The same server response (e.g. a password-policy violation on submit) is an actionable, retryable state on Android and an opaque generalError on iOS. This is the largest public-contract divergence in the PR, and it's in the good direction — but it means "mirrors the iOS V2 sign-up scenarios" isn't accurate for this branch.

Recommendation. Keep the Android behaviour and file the iOS gap, or hold this mapping back until iOS lands it. Either way, call it out explicitly in the PR description so the platforms don't quietly diverge.

4. Severity: MediumparseCollectAttributes has no self fallback for the submitAttributes link

Issue. iOS resolves the submit target defensively:

collectAttributesResponse.href(for: .submitAttributes) ?? collectAttributesResponse.href(for: .self)

(the same ?? .self fallback it uses for .update). Android returns missingLinkError(...) the moment links["submitAttributes"] is absent. Compounding it, self isn't in NativeAuthV2ContinuationState.SUPPORTED_RELATIONS, so a self href would be dropped from the continuation state even if the response carried one.

Impact. A collectAttributes body that only self-links — the exact shape iOS defends against — works on iOS and hard-fails mid-flow on Android.

Recommendation. Mirror the iOS fallback and add self to SUPPORTED_RELATIONS. If the fallback is deliberately omitted because the contract guarantees submitAttributes, please note that in the KDoc and file the iOS side as dead code.

5. Severity: Medium — the password wipe doesn't do what the docs claim

Issue. NativeAuthV2FlowController.upfrontAttributeValues:

values[ATTRIBUTE_NAME_PASSWORD] = String(password)

That materializes an immutable heap String, which then flows into NativeAuthV2SubmitAttributesRequestParameters.attributes (Map<String, String>), through the request body, and is never cleared. The finally { StringUtil.overwriteWithNull(parameters.password) } in signUpStart only zeroes the char[].

SignUpV2StartCommandParameters's Javadoc states "The password is never retained in continuation or public state; the controller clears the buffer once the request has been issued," and the inline comment says it's cleared "so it never outlives the request." Neither holds for the String copy — it lives until GC and can be captured in a heap dump.

Impact. Not a new leak relative to the rest of the SDK, but the documented security property is stronger than the implementation. That mismatch is the risk: a future reader will trust the comment.

Recommendation. Either (a) thread the password to the JSON body as char[]/CharSequence so no immutable copy is created, or (b) correct the Javadoc and inline comments to describe what actually happens. The same pattern appears in msal#2565 PasswordRequiredStateV2.submitSignUpPassword (mapOf("password" to String(passwordCopy))) with an equally strong comment.

6. Severity: Medium — attribute values are Map<String, String>; iOS uses [String: Any]

Issue. NativeAuthV2SubmitAttributesRequest, createSubmitAttributesRequest, performSubmitAttributes, SignUpV2StartCommandParameters.attributes, and NativeAuthV2SubmitAttributesCommandParameters.attributes are all Map<String, String>. iOS's MSALNativeAuthV2SubmitAttributesRequestBody.attributes is [String: Any], guarded by JSONSerialization.isValidJSONObject, so numeric/boolean CIAM extension attributes serialize with their native JSON type.

Impact. An extension_age: 30 attribute goes out as "30" from Android and 30 from iOS. If the directory schema types the attribute as a number, one of those is rejected.

Assumption: this may be a deliberate Android-wide constraint — V1 UserAttributes is also string-only — in which case disregard. But please confirm the v2 submitAttributes contract coerces stringified values for non-string attribute types; if it doesn't, this is a functional gap rather than a style difference.

7. Severity: Medium — no email-channel guard on the sign-up CodeRequired branch

Issue. iOS's handleSignUpInteractionResult guards the .verificationRequired case:

guard challenge.channelType.isEmailType else { /* general error:
    "Sign up currently supports email one-time-code verification only" */ }

Android's CodeRequired branch in handleSignUpInteractionResult surfaces whatever challengeChannel the server sent, with no check.

Impact. If a tenant is configured with SMS OTP for sign-up, Android hands the app a CodeRequiredStateV2 for a channel the flow was never validated against, while iOS fails fast with a clear message. Divergent and harder to diagnose.

Recommendation. Add the same guard, or document why Android intentionally accepts all channels.

8. Severity: MediumAttributesInvalid.invalidAttributes flattens unrelated detail entries

Issue.

serverError.details.flatMap { it.attributeIds }.distinct()

details is the full innerError.details array. Entries whose code is unrelated to the validation failure (e.g. a userAlreadyExists detail carried alongside, which your own fixture comment anticipates) contribute their attributeIds too.

Impact. The app can be told to correct an attribute the server didn't actually reject.

Recommendation. Filter to details that belong to the validation failure before flattening, or document that invalidAttributes is a superset.

9. Severity: Medium — upfront password / attributes are silently discarded unless the first response is collectAttributes

Issue. signUpStart passes upfront = parameters into handleSignUpInteractionResult, but every branch except AttributesRequired ignores it. If the server responds to the entry POST with verify or readyToComplete, the caller's password and attributes are dropped with no signal — the app then gets CodeRequired, and the password it supplied to signUpV2() never reaches the server.

iOS has the identical shape, so this is a shared design gap, not an Android regression. Flagging it because the "entry POST omits the username so we always get collectAttributes back" assumption is load-bearing and undefended on both platforms.

Recommendation. At minimum a Logger.warn when upfront is non-null and gets dropped, so the failure is diagnosable from logs.

10. Severity: MediumretryState for AttributesInvalid is the pre-submit state

Issue. performSignUpSubmitAttributes passes retryState = state — the state before withAdditionalSubmittedAttributes(...) is applied.

Impact. After a rejected upfront submit, the app retries from a state whose submittedAttributes is empty. If the server subsequently re-requests email (already sent), the flow surfaces AttributesRequired to the app instead of the intended "already submitted" hard error, defeating the loop-detection this PR adds.

Recommendation. Use state.withAdditionalSubmittedAttributes(attributes.keys) as the retry state. The continuation token is unchanged either way, so this is a pure bookkeeping fix.

11. Non-blocking — NativeAuthV2RequiredAttribute.required nullability

iOS coerces a missing required to false (attribute["required"] as? Bool ?? false). Android carries Boolean? all the way to the public RequiredUserAttribute.required, so apps see null on Android where iOS sees false. RequiredUserAttribute is a pre-existing V1 type so changing it isn't in scope here, but the V2 NativeAuthV2RequiredAttribute could default it.

Cycle summary

  • New issues: 11 (2 High, 8 Medium, 1 non-blocking)
  • Resolved issues:
  • Remaining blockers: Update Readme #1 (SignInAfterSignUpRequired unreachable from signUpStart — account created, generic error returned) and Updated readme. closes #1 #2 (userAlreadyExists wire-location contradiction with iOS).

Verified as not issues, for the record: parcel read/write symmetry in the new V2 states; routing the sign-up password through submitAttributes rather than the sign-in submit-password endpoint; submittedAttributes case-insensitivity (lowercase() ≈ iOS caseInsensitiveCompare); HAL action string values; and the absence of reserved-name filtering in the deferred submitAttributes path (iOS behaves the same).

Replied on main MSAL - AzureAD/microsoft-authentication-library-for-android#2565 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve the reset-password result contract, support password-only attribute submission, and document password buffer ownership.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce flow-specific command and result names while retaining the original generic command as a deprecated compatibility wrapper. This lets existing MSAL consumers continue compiling until they migrate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@djanardhan98

Disha Janardhan (djanardhan98) commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Commits:

•  747737bd2  — Separate Native Auth V2 sign-up submit-code flow
•  7093dbf46  — Add explicit reset-password submit-code names

Changes and rationale:

• Split sign-up submit-code results from the existing reset-password result contract. This prevents new sign-up outcomes from breaking exhaustive  when  expressions in existing MSAL consumers.
• Added  NativeAuthV2SignUpSubmitCodeCommand  and  submitSignUpCode()  for sign-up-specific outcomes.
• Added the symmetric reset-password names:
NativeAuthV2ResetPasswordSubmitCodeCommand 
NativeAuthV2ResetPasswordSubmitCodeCommandResult 
submitResetPasswordCode() 
• Retained  NativeAuthV2SubmitCodeCommand  and  submitCode()  as deprecated compatibility boundaries so Common can merge without requiring an immediate MSAL change.
• Added the requested TODO next to the compatibility command
• Kept  NativeAuthV2SubmitCodeCommandParameters  generic because sign-up and reset password intentionally share the same verification request.
• Shared the underlying verification/network implementation rather than duplicating provider logic.
• Allowed password-only sign-up attribute submission while continuing to reject requests containing neither attributes nor a password.
• Narrowed the SpotBugs  EI_EXPOSE_REP2  suppression to the generated password builder setter. This documents intentional password-buffer ownership transfer without suppressing findings for unrelated mutable fields.
• Added the dedicated sign-up telemetry API ID and regression tests.
• Merged the latest Common dev

@djanardhan98 Disha Janardhan (djanardhan98) added the changelog:minor Backward-compatible feature that requires a minor version increment. label Sep 9, 2026
@spetrescu84
Silviu Petrescu (spetrescu84) changed the base branch from djanardhan/native-auth-v2-signin to dev September 10, 2026 09:05
Copilot AI lite review requested due to automatic review settings September 10, 2026 09:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It introduces broad new sign-up flow contracts and controller orchestration across common/common4j, requiring careful human validation of API compatibility and edge-case behavior.

Pull request overview

Adds Native Auth V2 sign-up support across the common4j protocol layer and the common flow/controller layer, aligning Android’s V2 sign-up orchestration with the existing V2 sign-in/SSPR shape.

Changes:

  • Extend the V2 HAL parsing + interaction result model to support collectAttributes, sign-up specific error mapping, and “ready-to-complete → sign-in-after-sign-up” signaling.
  • Add sign-up request building + interactor operations (signUpStart, submitAttributes) with continuation-state bookkeeping for previously submitted credentials.
  • Add common controller orchestration + command/parameter/result contracts for sign-up start, submit-attributes, submit-code (sign-up), and explicit sign-in-after-sign-up; plus comprehensive unit tests and test fixtures.
File summaries
File Description
common4j/src/testFixtures/kotlin/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2ContinuationStateTestFactory.kt New fixture to generate representative continuation states for downstream tests.
common4j/src/test/com/microsoft/identity/common/java/nativeauth/providers/v2/NativeAuthV2SignUpRequestProviderTest.kt New tests validating sign-up request bodies/URLs and redaction behavior.
common4j/src/test/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2SignUpResponseParserTest.kt New tests for sign-up response parsing (collectAttributes, error mapping, ready-to-complete).
common4j/src/test/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2ContinuationStateTest.kt Extends continuation-state tests for sign-up attributes bookkeeping and new link relations.
common4j/src/test/com/microsoft/identity/common/java/nativeauth/providers/interactors/NativeAuthV2SignUpInteractorTest.kt New tests for sign-up interactor HTTP contract and credential-name retention behavior.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/v2/NativeAuthV2RequestProvider.kt Adds sign-up request creation + link resolution fallback logic.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/v2/NativeAuthV2FlowScenario.kt Adds SIGN_UP to flow scenario enum and updates documentation.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2ResponseParser.kt Adds collectAttributes parsing and sign-up-specific interaction error mapping.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2RequiredAttribute.kt New public model for required sign-up attributes to surface above common4j.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2LinkRelation.kt Adds SUBMIT_ATTRIBUTES and SELF link relations.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2InteractionApiResult.kt Adds sign-up interaction results: AttributesRequired, UserAlreadyExists, InvalidAttributes.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2HalApiResponse.kt Extends HAL response model with required attributes array + error details parsing.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/responses/v2/NativeAuthV2ContinuationState.kt Tracks submitted credential names across sign-up steps and expands supported relations.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/requests/v2/NativeAuthV2SubmitAttributesRequest.kt New request model for sign-up submit-attributes (password handled as erasable buffer).
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/requests/v2/NativeAuthV2SignUpStartRequest.kt New request model for sign-up start (continuation-token only).
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/NativeAuthV2OAuth2Strategy.kt Adds strategy entry points for sign-up start and submit-attributes.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/providers/interactors/NativeAuthV2Interactor.kt Adds sign-up entry + submit-attributes network operations (with password buffer clearing).
common4j/src/main/com/microsoft/identity/common/java/nativeauth/controllers/results/SignUpCommandResult.kt Expands InvalidAttributes contract to cover V2 sign-up command results.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/controllers/results/NativeAuthV2CommandResult.kt Adds sign-up command results + refines submit-code marker interfaces/contracts.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/controllers/results/INativeAuthCommandResult.kt Updates result marker interfaces to include new V2 sign-up contracts.
common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/SignUpV2StartCommandParameters.java New V2 sign-up start parameters (username + optional password + optional attributes).
common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/NativeAuthV2SubmitAttributesCommandParameters.java New submit-attributes parameters (attributes + optional erasable password + continuation state).
common4j/src/main/com/microsoft/identity/common/java/nativeauth/commands/parameters/NativeAuthV2SignInAfterSignUpCommandParameters.java New explicit sign-in-after-sign-up parameters for token exchange + cache persistence.
common4j/src/main/com/microsoft/identity/common/java/eststelemetry/PublicApiId.java Adds public API IDs for the new V2 sign-up operations.
common/src/test/java/com/microsoft/identity/common/nativeauth/internal/controllers/v2/NativeAuthV2SignUpFlowControllerTest.kt New unit test suite for sign-up orchestration and completion behavior in controller.
common/src/test/java/com/microsoft/identity/common/nativeauth/internal/controllers/v2/NativeAuthV2FlowControllerTest.kt Minor adjustment to reset-password submit-code test expectations.
common/src/test/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2CommandsTest.kt Adds coverage for dedicated reset-password/sign-up submit-code command entry points.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/controllers/v2/NativeAuthV2FlowController.kt Adds sign-up start/submit-attributes/sign-up submit-code/sign-in-after-sign-up orchestration and reserved-attribute validation.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2SubmitCodeCommand.kt Deprecates the generic submit-code command as a compatibility wrapper for reset-password submit-code.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2SubmitAttributesCommand.kt New command for sign-up submit-attributes step.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2SignUpSubmitCodeCommand.kt New command for sign-up submit-code step.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2SignUpStartCommand.kt New command for sign-up start.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2SignInAfterSignUpCommand.kt New command for explicit sign-in-after-sign-up completion.
common/src/main/java/com/microsoft/identity/common/nativeauth/internal/commands/NativeAuthV2ResetPasswordSubmitCodeCommand.kt New command for reset-password submit-code with explicit flow naming.
Review details
  • Files reviewed: 34/34 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Resolve the continuation-state fixture merge by retaining configurable flow support for downstream MSAL tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
data class SignInAfterSignUpRequired(
override val correlationId: String,
val continuationState: NativeAuthV2ContinuationState,
) : NativeAuthV2SubmitAttributesCommandResult,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 ReadyToComplete  can be returned by the upfront attribute submission performed inside  signUpStart() . In that case this result is passed back to  signUpStart , but it does not implement  NativeAuthV2SignUpStartCommandResult , so the cast in  NativeAuthV2FlowController.signUpStart()  fails and converts a successful registration into  unexpectedSignUpApiError() . Can we check this once?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first submitAttributes cannot return readyToComplete from the server as verify on email (and in future sms etc) needs to happen. If readyToComplete happens, it should return the error I would say. This happens because NativeAuthV2SubmitAttributesCommandResult can be done multiple times and at some point readyToComplete (after verify) is ok to be returned

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog:minor Backward-compatible feature that requires a minor version increment.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants