Phishing-resistant MFA and a closed device authorization grant are the two controls that answer the campaign Microsoft described on 9 September 2026. Attackers are not breaking passkeys. They are calling users, pretending to be the helpdesk, and talking them into either completing a login on an adversary-in-the-middle proxy or approving a device code that belongs to the attacker. Both routes end with a legitimate session token issued by the real identity provider. In Keycloak the fix has two halves: make the authenticator origin-bound so a relayed login produces nothing usable, and keep the device authorization grant off for every client that does not genuinely need it. Keycloak 26.x ships both, and the device grant is already off per client by default, so the work is an audit rather than a global switch.
The uncomfortable detail in this campaign is that ordinary MFA is not a defense against either route. A relayed one-time code still authenticates, and a device code approval does not involve the second factor at all.
What did Microsoft observe in the September 2026 campaign?
In 2026, Microsoft Security Research reported that attackers open with phone calls or SMS impersonating IT helpdesk staff, then steer victims to either an adversary-in-the-middle phishing page that mirrors the Microsoft sign-in screen, or a device code authorization flow where the victim authorizes the attacker’s client on the real login page (Microsoft Security Blog, “Passkey-themed social engineering leads to identity and cloud compromise,” 9 September 2026).
What follows the initial access is the part worth copying into your own detection rules. Microsoft describes attackers registering their own MFA methods on the compromised account, phone, authenticator app, or software OTP, then using Microsoft Graph to inventory users, groups, permissions, resources and accessible content across the tenant before collecting data from SharePoint, OneDrive and Exchange over several hours to multiple days. Microsoft attributes activity in this cluster to actors it tracks as Storm-3121 and Storm-3032, feeding downstream extortion brands.
Microsoft’s own recommendations are short and specific: “Enforce phishing-resistant MFA (FIDO2/passkeys, Windows Hello for Business) via Conditional Access” and “Block the device code and authentication transfer flows via Conditional Access, except where an explicit business need exists.” That second one translates cleanly to Keycloak, and the translation is easier than the Entra version.
Why does a passkey lure work when the victim already has MFA?
The lure works because the attacker never needs to defeat the authenticator. Both routes in this campaign end with the real identity provider issuing a real session to a user who really authenticated. An AiTM proxy sits between the victim and the login page, relays every field including the one-time code, and keeps the resulting session cookie. The device code route does not even need a proxy: the victim reads a code aloud or types it into the genuine verification page and consents.
That is why the theme of the lure is almost beside the point. “We are enrolling you in passkeys” is a pretext with unusually good timing, because plenty of organizations genuinely are, so a call about it lands as plausible rather than suspicious.
Origin binding is the property that breaks the first route. A WebAuthn credential signs a challenge that includes the relying party identifier, so a credential registered for login.example.com produces a signature the attacker’s proxy domain cannot use. Nothing the user types can be relayed, because nothing useful is typed. Push approvals and authenticator-app codes both fail this test, which is the gap that separates “we have MFA” from “we have phishing-resistant MFA.”
The second route is not solved by better authenticators at all. A perfectly phishing-resistant login still produces a token if the user consents to the wrong device code, which is why the grant itself has to be scoped rather than hardened.
Is Keycloak’s device authorization grant on by default?
No, and this is the single most useful fact for anyone auditing a realm after reading the Microsoft report. Keycloak ships the OAuth 2.0 Device Authorization Grant as a supported, default-enabled server feature, but the per-client switch that actually permits the grant is off unless somebody turns it on. In the client representation the attribute is oauth2.device.authorization.grant.enabled, and OAuth2DeviceConfig.isOAuth2DeviceAuthorizationGrantEnabled parses it with Boolean.parseBoolean, so an absent attribute resolves to false (Keycloak source, OAuth2DeviceConfig, release 26.7 branch).
So the Keycloak equivalent of Microsoft’s “block device code flows” is not a global kill switch you have to go find. It is an audit: list your clients, find the ones where that attribute is true, and justify each one.
# Every client in a realm with the device grant switched on
kcadm.sh get clients -r my-realm
--fields clientId,attributes
| jq -r '.[] | select(.attributes["oauth2.device.authorization.grant.enabled"] == "true") | .clientId'
In the admin console the same switch lives under Clients, then your client, then Settings, in the Capability config block labelled OAuth 2.0 Device Authorization Grant. If the list comes back longer than the set of CLIs, kiosks and TV apps you can name, the difference is what needs justifying.
Two dials shape the blast radius for the clients that keep the grant. Keycloak defaults the device code lifespan to 600 seconds and the polling interval to 5 seconds (Keycloak source, OAuth2DeviceConfig, release 26.7 branch). Both can be overridden per client with oauth2.device.code.lifespan and oauth2.device.polling.interval. Ten minutes is generous for a social engineering call that has to keep a victim on the phone, so shortening it for the clients that keep the grant narrows the attacker’s window. Pick the floor from your own devices: a user who has to find a phone, open the verification URL and complete a passkey ceremony can take longer than you would guess, so measure before you set it rather than reaching for two minutes because it sounds tight.
How does origin binding stop a relayed login in Keycloak?
Passkeys became a supported feature in Keycloak 26.4, after arriving in the default authentication forms as a preview in 26.3, so the work is realm configuration rather than a build. The complete guide to Keycloak passkeys and WebAuthn covers the full setup. For this threat model there are two screens, and conflating them is the usual reason people cannot find the switch. The Enable Passkeys toggle is in Realm Settings, then the Login tab, in the Login Screen Customization section. The credential rules, including requiring discoverable credentials, live in the WebAuthn Passwordless Policy under Authentication, then Policies, which the settings icon next to the toggle links straight to. On Skycloak the same policy surfaces are documented under multi-factor authentication and passwordless.
One version note if you are inheriting an older realm: the separate passkeys conditional UI authenticator is marked deprecated in the 26.7 source, so a realm still wired to it should move to the integrated forms.
Three configuration choices matter more than the rest once you are defending against relay attacks:
- Set the relying party ID deliberately. It is the origin binding. Set it to the registrable domain your login pages actually live on, and understand that credentials registered under one RP ID will not work under another.
- Require user verification. Without it, a stolen or borrowed authenticator is a single factor.
- Require attestation only if you actually consume it. Most teams do not, and requiring it mostly generates support tickets.
The harder part is coverage, not configuration. An attacker who can reach any user still holding a phishable factor has a way in, so partial rollouts buy less than the rollout percentage suggests. If you need a staged path, level of authentication mapping lets you require passkeys for sensitive operations while general sign-in still accepts a weaker factor during transition, and Entra’s own SMS retirement timeline is a useful forcing function if you need one to point at in a planning meeting.
For the application side of a passkey rollout, the React and Next.js passkey integration walkthrough covers what changes in the client.
How do you cut off an attacker after a suspected helpdesk lure?
Revoking one session is not enough, because the token the attacker holds is not the only artifact they left behind. Work the list in this order.
Kill the sessions. Keycloak’s admin REST API logs a user out of every session with a single call, which also invalidates the refresh tokens issued against them.
# Log the user out everywhere
curl -X POST
-H "Authorization: Bearer $ADMIN_TOKEN"
"$KC_URL/admin/realms/my-realm/users/$USER_ID/logout"
If you need surgical removal rather than a full logout, the guide to revoking a single Keycloak session covers the session-scoped endpoints and their limits.
Clean up the offline sessions. The logout endpoint above also sets a not-before timestamp on the user, and Keycloak rejects any refresh token issued before it, offline tokens included. That is the part people usually get wrong in both directions: an attacker’s offline token does not survive the admin logout, but the offline session records do, and the user’s consent to the client remains. Revoking that consent with DELETE /admin/realms/{realm}/users/{id}/consents/{client} removes the offline token along with it, which is the cleaner end state after a confirmed compromise.
Audit the authenticators. This is the step Microsoft’s report makes non-negotiable. Attackers register their own MFA method as persistence. Check the user’s credentials in the admin console, or pull them from the API, and treat any credential created around the incident window as attacker-controlled until proven otherwise.
curl -H "Authorization: Bearer $ADMIN_TOKEN"
"$KC_URL/admin/realms/my-realm/users/$USER_ID/credentials"
Shorten the standing window generally. If your access token lifespan is measured in hours, every incident response starts further behind. Refresh token rotation with Revoke Refresh Token enabled turns a stolen refresh token into a detectable event rather than a silent renewal.
What should you log and alert on?
A handful of Keycloak event types cover most of this campaign’s footprint, and event capture is off by default in a fresh realm. Turn on user events under Realm settings, then the Events tab, then User events settings, and do the same for admin events, before you need them.
| Signal | Event to watch | Why it matters here |
|---|---|---|
| Attacker MFA persistence | UPDATE_CREDENTIAL, REMOVE_CREDENTIAL |
New authenticator registered shortly after a login is the campaign’s signature |
| Device grant use | OAUTH2_DEVICE_AUTH, OAUTH2_DEVICE_VERIFY_USER_CODE, OAUTH2_DEVICE_CODE_TO_TOKEN |
Device authorizations on a client that should not have the grant at all |
| Session anomalies | LOGIN and REFRESH_TOKEN with unfamiliar IP or user agent |
Automation against the realm shows up here, though the bulk-collection traffic Microsoft observed would land in your API gateway logs rather than Keycloak’s |
| Consent | GRANT_CONSENT |
Consent to a client nobody in your organization published |
Getting these somewhere you will actually read them is the other half. The walkthrough on forwarding Keycloak events to a SIEM over webhook covers the plumbing, and Keycloak auditing and event logging covers what each event type actually contains. Skycloak surfaces the same stream through audit logs and session management, which is where the revocation steps above are driven from.
One practical note from running these realms: the credential-change alert is the one that pays for itself. It fires rarely, it is cheap to tune, and in this attack pattern it is the first signal that appears after a successful compromise rather than during it.
What do you keep when you genuinely need the device grant?
Plenty of legitimate workloads need it. CLI tools, smart TVs, kiosks, headless IoT devices and anything without a browser all depend on the flow, and the full device flow implementation guide covers building against it properly. Keeping it safely comes down to four decisions:
- Scope the grant to specific clients, never as a realm-wide habit. The default is already
false; keep it that way for everything that does not need it. - Shorten the code lifespan on those clients. A device code valid for two minutes is very hard to walk somebody through over the phone.
- Require consent on device-grant clients so the verification page names the application and the scopes, giving a suspicious user something concrete to be suspicious about.
- Teach one sentence. Nobody from IT will ever ask you to read a code to them or type a code they gave you. That sentence covers device code phishing and OTP relay at the same time, and it is short enough that people remember it.
One thing to keep separate from all of this is the device flow lockout CVE from earlier in 2026, which is a brute-force accounting bug rather than social engineering and should be patched on its own schedule.
Where does managed Keycloak change this?
The controls above are all upstream Keycloak features, available to anyone running the project themselves. What changes with a managed deployment is whether the realm defaults, the event pipeline and the patch cadence stay correct six months after the hardening sprint, when the person who ran it has moved to another team.
Drift is the failure mode to plan for. A common pattern is a device grant nobody deliberately enabled, arriving because a client was copied from another client or imported from a realm export that predated the policy. Event logging switched on during an incident and left at default retention afterward is the same shape of problem.
Skycloak runs upstream Keycloak, not a fork, so every control in this post behaves as upstream documents it. The difference is that realm baselines, event forwarding and version currency are owned rather than intended. If your team is weighing that trade-off, the accounting is in is self-hosting Keycloak worth it in 2026.
Frequently asked questions
Are passkeys phishable?
The credential itself is not, because a WebAuthn assertion is bound to the relying party origin and cannot be replayed to a different domain. What remains phishable is everything around it: an account recovery path, a weaker fallback factor left enabled, or a device authorization code the user approves. The September 2026 campaign attacks those, not the cryptography.
Does blocking the device code flow break anything?
Only clients that use it, which in most realms is a short and nameable list: CLI tools, kiosks, TV apps and headless devices. In Keycloak the grant is already off per client by default, so the audit is about finding clients where somebody switched it on rather than turning something off globally.
What is the difference between AiTM phishing and device code phishing?
An adversary-in-the-middle attack proxies the real login page and captures the resulting session cookie, so the victim believes they are signing in normally. Device code phishing skips the proxy: the attacker starts a device authorization request and persuades the victim to approve the attacker’s code on the genuine verification page. Both end with a real token.
How do I tell whether an attacker registered their own MFA method?
Query the user’s credentials through the admin REST API or check the Credentials tab in the admin console, and compare creation timestamps against the suspected incident window. Enabling user events captures UPDATE_CREDENTIAL so the registration shows up in your event stream rather than only in current state.
Is one-time code MFA still worth keeping?
As a fallback during migration, yes, and as a destination, no. Authenticator-app codes and push approvals both stop credential stuffing and password reuse, which is most attacks. They do not stop a real-time relay, which is this one. Treat them as a step on the way to origin-bound credentials rather than the end state.
Does Keycloak support conditional access based on device compliance?
Not in the form Entra means by Conditional Access. Keycloak gives you conditional authentication flows keyed on things like role, client scope, user attribute, credential type and level of authentication, which covers step-up and risk-tiering. Device compliance signals have to come from an external source and be fed in, typically through a custom authenticator or an identity provider that already holds them.
Sources
- Microsoft Security Blog, “Passkey-themed social engineering leads to identity and cloud compromise,” published 9 September 2026, retrieved 2026-09-16, https://www.microsoft.com/en-us/security/blog/2026/09/09/passkey-themed-social-engineering-leads-identity-cloud-compromise/
- Keycloak,
OAuth2DeviceConfigsource, default device code lifespan of 600 seconds and polling interval of 5 seconds, release 26.7 branch, retrieved 2026-09-16, https://github.com/keycloak/keycloak/blob/release/26.7/server-spi/src/main/java/org/keycloak/models/OAuth2DeviceConfig.java - Keycloak, “Passkeys,” server administration guide, for the Realm Settings Login tab location of the Enable Passkeys switch, release 26.7, retrieved 2026-09-16, https://github.com/keycloak/keycloak/blob/release/26.7/docs/documentation/server_admin/topics/authentication/passkeys.adoc
- Keycloak,
EventTypesource, for the event names used in the detection table, retrieved 2026-09-16, https://github.com/keycloak/keycloak/blob/main/server-spi-private/src/main/java/org/keycloak/events/EventType.java - Keycloak,
UserResourcesource, admin logout setting a not-before timestamp for the user, release 26.7 branch, retrieved 2026-09-16, https://github.com/keycloak/keycloak/blob/release/26.7/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java - Keycloak,
Profile.Featuresource, passkeys and the device authorization grant as default-enabled features in the 26.7 line, retrieved 2026-09-16, https://github.com/keycloak/keycloak/blob/release/26.7/common/src/main/java/org/keycloak/common/Profile.java - IETF, “RFC 8628: OAuth 2.0 Device Authorization Grant,” retrieved 2026-09-16, https://datatracker.ietf.org/doc/html/rfc8628