This post was originally published on Medium.
This post is the third in a series; in the first, we discuss the app-to-web session transfer problem and propose a secure approach using OAuth 2.0 Pushed Authorisation Requests (PAR). In the second, we walked through a detailed implementation blueprint using the PingOne platform. This third part will dive specifically into the security implications of this approach and a number of potential enhancements we could consider. Familiarity with the previous two parts is recommended before diving into this one.
Understanding the risks
First, a disclaimer. What follows is not exhaustive — nor could it ever be. I urge anyone reading and following this advice to remember that what I’m providing here is certainly not meant to be iron-clad security guidance, however well-reasoned I like to think it may be!
Nothing that we implement in any scenario should ever be considered 100% secure anyway (not in a world where we have integrated systems with boundaries — not to mention real-life users!) Our challenge is always to do what we can to predict the threats we might face, assess the likelihood and impact of each such risk and then critically assess our proposed solution against both aspects.
If we consider the pattern end-to-end, we should be able to identify a number of potential weak points where a motivated and skilled adversary could compromise security. The first is not a new or additional risk factor, given that we are using OAuth 2 to secure a native mobile application since it relates to the usage of an access token as a bearer token to allow API access. This implies that the utmost care must be taken within the end-to-end system architecture (but particularly within the client app itself) to protect this access token from unauthorised access or exfiltration.
We can argue that our PAR-based approach does not create any additional risk of access token exposure beyond what is already present anyway. When our application makes an authenticated call to a backend API, it initiates an HTTPS request and includes the access token in an authorisation header. The PAR approach includes the access token as a POST body parameter over HTTPS to our authorisation server and since we trust the server and the orchestration logic it executes, this should not materially affect our exposure risk.
Additional controls
We have added a number of additional controls to guard against specific risks, including the following:
-
We use a dedicated session transfer client to initiate the PAR process and intentionally restrict this client’s capabilities with the “web-session-only” scope. By configuring this client with a meaningless scope, an invalid redirect_URI and by including a PKCE Code Challenge we add a number of layered obstacles to prevent misuse of this client — that is in addition to specific orchestration logic to prevent the successful issue of any access token to this client.
-
The PAR response contains a request_uri parameter and this, while inherently single-use and short-lived, does create a potential security risk since it could be exfiltrated and copied to a different device in order to hijack the web session that it creates. We mitigate this risk by further constraining the useful life of the request (via our 10-second exp claim window) and by performing an IP check ahead of creating the web session.
The missing piece: device binding
One significant risk that we have not adequately addressed is that of access token exfiltration followed by a spoofed PAR request from a different client. Given that the access token itself is all that conveys the authenticated user context, we need to ensure that should an attacker obtain a valid access token by some nefarious means, they would not be able to use this token to create a valid web session on a different device.
Our remaining challenge is thus to implement some mechanism to ensure that PAR request is initiated (and completed) on the same device that obtained the access token initially.
We should, at this point, look critically at the decision to use IP address as the mechanism for binding the backend and front-end requests. While it is arguably the simplest thing to implement, IP address is typically a rather weak proxy for real device identity since it can be almost trivially replicated via proxy.
It is tempting to consider binding the access token the device IP address at the time of initial login and this approach certainly would be more secure than simply echoing the current IP at the time of the PAR call. As a counter, though, the nature of mobile devices means that we should expect that a device’s IP address could change during the course of a session as the user moves from one network to another. We could end up restricting our application to a single IP address for the duration of the session if we were to follow this approach.
Extending the solution with PingOne MFA
Within our toolkit, we have the PingOne MFA service, Ping Identity’s strong authentication solution and one simple way for us to further improve our security posture here would be to use the Automatic Device Authorisation capability offered by the PingOne MFA Mobile SDK. This approach allows us to do the following:
- identify a device through a cryptographic payload
- bind that device to a user record
- perform ad-hoc verification of that device based on a provided payload
Let’s revisit the end-to-end process description from two posts back — looking at how PingOne MFA is used to extend and further secure the flow.
-
When Alice first registers her account with the Acme Inc native application, the app code embeds the PingOne MFA SDK, which generates a cryptographic device payload (bound to a private key in Alice’s key-store). This payload is passed to the PingOne backend as part of Alice’s registration request where the associated public key is paired with Alice’s user profile. The device is thus bound to Alice’s account.
-
Alice opens the Acme Inc native application and clicks the “Login” button. The app uses the PingOne MFA SDK to generate a signed payload and includes this payload with the authentication request sent to the PingOne backend.
-
She is authenticated via a native authentication experience (no browser pop-up) that uses DaVinci flow logic to collect Alice’s user credentials, validate these against the user directory and then calls the PingOne MFA backend to validate the payload from the device and ensure that the extracted device ID is bound to Alice’s account.
-
The flow completes successfully and includes the device ID as a claim in the access token that is sent to the app. The app responsibly and securely stores this access token (which is bound to her device via the device_ID claim) on Alice’s behalf.
-
Alice clicks the “Manage my profile” button in the app. The app code starts the PAR process using the client_id of the dedicated Session Transfer Client. It includes a scope such as “session_transfer” in the request, and generates a PKCE Code Challenge that it includes with the request. Note that the corresponding Code Verifier can be discarded at this point since this flow is not intended to ever result in an access token issuance.
-
The app code makes a call to the PingOne MFA SDK to generate a fresh device payload. It then includes this payload, along with the access token that it already holds for the user, and the URL of the profile management application, in the PAR body that it constructs.
-
It sends the PAR request to the authorisation server and obtains a response that contains a request_uri, which it appends to the AS’s authorization endpoint URL before opening a browser tab to initiate the front-channel flow.
-
When the front-channel request hits the Authorisation Server, the request is validated (to ensure it has not expired or already been used) before being handed to the identity orchestration engine for further processing.
-
The DaVinci orchestration engine must introspect or validate the provided access token to ensure that it is valid, has not expired and was indeed issued to the expected client (the mobile app). It should also extract the device_ID claim from the token. It then calls the PingOne MFA backend to validate the payload from the device and ensure that the extracted device ID is bound to Alice’s account. It also ensures that the device ID extracted from the device payload matches the device_ID claim extracted from the access token.
-
Should the above checks succeed, the orchestration engine extracts the “sub” claim from the access token to determine the appropriate user, and sets a session cookie in the browser for that same user.
-
At this point, the orchestration logic simply redirects the browser to the URL of the profile management application (the same URL that was provided via the PAR back-channel initiation in step 6). This is the step that interrupts the flow started by the Session Transfer Client and ensure that no auth code or access token is ever issued.
-
The profile management application now receives the browser redirect and is responsible for initiating its own OAuth/OpenID Connect Auth Code flow, just as it would had a browser opened the app URL directly. It should generate its own redirect to the AS, including its own client_id and redirect_uri, its own appropriate scopes and its own PKCE Code Challenge. The AS will start a user authentication process that should complete with no further user interaction required, based on the presence of the session cookie set in step 10.
-
The browser is thus redirected back to the profile management application with everything that is needed to display logged-in user content.
The above approach certainly closes a number of potential security loopholes; however the lift is admittedly rather heavy as a result. I would advise a comprehensive review of your own risk landscape (in consultation with your security team) in order to decide whether the additional overhead is warranted in your specific scenario.
In conclusion
This has been quite the journey — from a frustrated user staring at a “Contextual Cliff” to a high-assurance architecture that even a cynical CISO could love.
To wrap up this series, let’s look at the path we’ve carved out:
- Part 1: The Blueprint. We defined the “Session Transfer Problem” and why relying on the “serendipitous” creation of browser cookies is no longer a viable strategy in a privacy-first, sandboxed world.
- Part 2: The Implementation. We got our hands dirty with Kotlin and DaVinci, moving sensitive session data off the URL and into the secure back-channel via OAuth 2.0 Pushed Authorization Requests (PAR).
- Part 3: The Hardening. We moved from “making it work” to “making it resilient,” exploring the nuances of IP binding and the “gold standard” of cryptographic MFA Device Binding to ensure the session stays where it belongs.
The reality of modern digital products is that hybrid architectures aren’t going anywhere. Our job as identity professionals isn’t to fight the existence of web-views, but to ensure that the transition between native and web remains invisible to the user and invincible to the attacker.
Identity orchestration gives us the tools to build these “bridges” without compromising on either side of the UX-Security trade-off.
Over to You
I’d love to hear from the community:
- How are you currently tackling session bridging in your own environments?
- Is the “heavy lift” of MFA-backed device binding a non-starter for your product teams, or has it become a non-negotiable requirement for your security auditors?
- Are there other “Contextual Cliffs” in your applications that you’re struggling to smooth over?
Drop a comment in the developer community with your thoughts, questions, or your own battle stories from the IAM trenches!
Do you have thoughts or questions on this article? Join the discussion on the Ping Identity developer community.
