September 1 is days away, and a lot of tenants are going to meet it by accident. Microsoft’s passkeys-by-default rollout has been on the calendar since July, but the part most admins missed is buried at the bottom of the retirement documentation: there is a temporary opt-out. One tenant-level property, one Graph call, and September 1 doesn’t happen to you.
I want to be careful about how I frame that, because “opt-out” is a word that invites the wrong reaction. This is not an escape hatch. It’s a pause button with a hard expiry, and the only tenants that should press it are the ones who know what they’ll do with the time. But knowing the button exists — and knowing how to press it without breaking anything else — is worth having before the date rather than after.
What September 1 Actually Does to a Tenant
Two things change automatically, and neither waits for your approval.
Users enabled for SMS or voice in the Authentication Methods Policy — or in legacy per-user MFA settings — get auto-enabled for passkeys, dropped into a passkey profile that allows all passkey types, synced and device-bound alike. At the same time, your Registration Campaign is set to Microsoft-managed state, targeting passkeys, and those same users are pulled into scope automatically. Next time they sign in and clear MFA, they get nudged to register.
The sharp edge, which I wrote about when MC1426371 first landed, is that the default snooze behavior under Microsoft-managed state is unlimited. Users can dismiss the prompt forever. So the automatic path reliably gets your users prompted and does nothing to get them migrated. It solves awareness and leaves completion entirely on your desk.
For a single tenant that’s a manageable surprise. For an MSP with dozens of them, it’s dozens of simultaneous changes to a security-relevant policy, on a date nobody scheduled, with helpdesk calls arriving from clients who were never told. That’s the real argument for the opt-out — not avoiding passkeys, but refusing to let a vendor pick your change window.
The Opt-Out, and What It Doesn’t Buy
The setting is optOutSettings.passkeyDynamicMigration on the Authentication Methods policy. Set it to true and your tenant is excluded from the automatic passkey enablement and the registration campaign rollout for the opt-out period. It lives on the Graph beta endpoint and needs the Policy.ReadWrite.AuthenticationMethod permission — plus an administrator role that can actually write the policy, such as Authentication Policy Administrator. A granted scope alone still returns 403 without the role behind it.
Here’s the part that has to be said in the same breath. The opt-out covers the September 1, 2026 through February 1, 2027 changes and nothing beyond that. On February 1, Microsoft-provided SMS and voice delivery is retired outright. After that date, any user whose only available MFA method is SMS or voice will be required to register a passkey during sign-in — a blocking prompt, not a nudge. They don’t get into their account until they register. Microsoft’s documentation states it about as plainly as Microsoft ever states anything: there is no opt out from the February 1 behavior, and it is enforced for all tenants.
So the honest description of what this property buys is five months of control over your own timeline. If you genuinely need telephony for a regulated segment, that’s the window to review the telecom providers appearing in the Microsoft Security Store from September 18 and configure one from October 30. If you don’t, that’s the window to run a registration campaign you configured, with your snooze limits, on your schedule. Setting the opt-out and then doing nothing until February just relocates the emergency and makes it worse — because when it lands, it lands as a blocking prompt instead of a skippable one.
Why This Needed a Script and Not a One-Liner
It’s a single boolean. It should be a single Invoke-MgGraphRequest and a coffee break. The reason it isn’t is the most interesting thing I learned building this.
optOutSettings does not appear in Microsoft’s published Graph metadata. A plain GET on the policy returns it, but ?$select=optOutSettings fails with BadRequest — the property is served in the payload without being part of Graph’s queryable model. That’s not a cosmetic detail. OData services are permitted to silently ignore properties they don’t model, accepting your request and returning success without changing anything at all. Which means a 200 OK on that PATCH does not prove the write landed.
That is a genuinely dangerous failure mode, and it’s dangerous in exactly the direction that hurts most: you run the command, you get a success response, you tick the tenant off your list, and on September 1 the rollout happens anyway. A silent no-op that looks identical to a success is worse than an error, because an error at least tells you to look.
The only defense is to re-read the value afterward and confirm it. So the script does the write, re-reads the policy, and throws a terminating error if the result isn’t true. A run only prints its FINAL VERIFIED STATUS block after the value has been read back and confirmed — if that block is absent, nothing was written. Verification isn’t a nicety bolted onto the end; it’s the entire reason the tool exists rather than a copy-pasted PATCH body.
Worth stating the limitation just as clearly: because the property is absent from published metadata, there’s no schema to validate the request shape against. It came from observed behavior against one production tenant, where it worked and the policy’s own lastModifiedDateTime advanced to confirm it. That’s the strongest evidence available and it’s still a sample of one, and an unmodelled beta property carries no compatibility guarantee. Which is precisely why a failed verification produces a loud error naming the problem — never a false success.
Report First, Write Once
The other thing I wanted from this tool was the ability to point it at a client tenant without holding my breath, so the design is built around a boundary rather than a feature list.
There is exactly one write call in the entire script, and it touches one property. Everything else is read-only. The full report prints on every run — -ReportOnly isn’t a separate first pass you have to make, it’s the same report with a stop sign where the write would be, telling you what a normal run would do and ending without the verified-status block. It never connects or disconnects Graph on your behalf — it uses your existing session, so a failed run doesn’t cost you your connection. It takes no tenant ID parameter at all; it acts on whatever tenant you’re already connected to and prints the account and tenant before doing anything, because the way you break a client is by being confidently connected to the wrong one.
And it explicitly does not touch Conditional Access, Security Defaults, per-user MFA, SSPR, legacy MFA, the migration state, any method’s state or targeting, or the registration campaign. It reads no per-user data — policy configuration only. It writes no files.
The report you get on the way is the part I’ve come to use most. Every authentication method the tenant returns, including disabled ones and any method the script doesn’t recognize, with state and targeting side by side — because a method enabled for one group is a completely different posture from one enabled for everybody, and the state alone will lie to you about that. Group GUIDs resolved to names where permissions allow, falling back gracefully when they don’t. The migration state, where only migrationComplete is treated as authoritative, since a null is never safe to read as “finished.” Closing observations worded as observations, not compliance findings, because enabling an authentication method doesn’t by itself require MFA and this policy can’t see the controls that do.
Half the value turned out to be the read, not the write. Running it across a fleet is the fastest inventory of where each tenant actually stands before September 1.
The Real Work Is Still the Migration
I’d rather leave you with the caveat than the tool. The opt-out is a scheduling instrument. It converts an imposed date into a chosen one, and that’s genuinely valuable — but it converts nothing else. The migration still has to happen, the admin accounts with hardened passkey profiles still get skipped by the registration campaign and still need a TAP-and-Conditional-Access path of their own, and February 1 still arrives with a blocking prompt for anyone left on SMS.
Use the pause to run the migration deliberately. Don’t use it to stop thinking about the migration.
→ entra-passkey-dynamic-migration — the script, the read-only shape probe, and full documentation covering every step from installing the module to confirming the change. MIT licensed. Run it with -ReportOnly first.
For the migration itself — passkey profiles, the device-bound versus synced distinction, registration campaign snooze logic, and the two-profile pattern that keeps admins hardened while everyone else moves — that runbook lives in my MSP Microsoft 365 knowledge base on GitHub.