OAuth 2.0 is an awesome standard. It has made it much easier to discuss standard security flows, uses gloriously simple REST APIs and provides a very robust mechanism by which to provide third-party authorisation. Unfortunately, while the third party authorisation mechanisms are great for web clients, the fact that they rely upon browser redirects or collecting credentials in order to use the Resource Owner Password flow make them less awesome for third parties who are delivering API-driven experiences via mobile apps or smart devices.
In this scenario, we are looking for a mechanism by which we can provide informed consent to resource owners; doesn’t risk exposing user credentials to third parties; and can be API-driven for the third-party, not relying upon browser behaviour. In this post we will explore an approach that checks all of these boxes, something that I have called an ‘Out-of-band Consent’, in which credentials and consent are handled through a process which is entirely removed from the third-party requesting application.
This flow is shown below:
Perhaps the easiest way to understand what is going on in this flow is from the perspective of a user, who should have a high degree of assurance that their data, and credentials, are being handled appropriately. A flow begins with some sort of third party application requesting authorisation, which to the user looks something like the following:
Here, our API-driven application is some sort of Smart TV, which technically may be able to display a browser and handle the redirects required for an OAuth 3-legged flow, but in reality is unlikely to do so, and, in addition, is terrible for entering data. Also, an in-TV browser hardly represents a completely trustworthy interface through which to enter user credentials (though MFA would certainly help).
The next stage of authorisation takes place out of band, in that the initial request is made from the Smart TV which creates the above request ID, but the authentication and authorisation takes place through a channel provided by the trusted provider. The user logs into the identity provider’s service through their normal channel, be it a webpage, mobile app, or something else.
By taking this approach, the user is assured that their credentials are being protected. They were the ones who initialised this log-in session, it wasn’t a potential phishing overlay from the third party, a malicious link, etc. The user instead used their normal channel. This also allows for any additional login factors, or whatever sorts of authentication behaviour are required from the identity provider’s side, and can take advantage of existing sessions.
Once logged in, the user is offered an option to ‘Authorise an Application’, and once navigated to, the user can the request ID which was displayed to them earlier on the Smart TV device. This can’t be prompted, as the initial request has no user context, and as such, there is no way to associate this log-in with the request for authorisation until after the request ID is provided.
This ID ought to be designed to be simple, as it needs to be retyped as the user transitions to the trusted channel, which may be across devices, so copying and pasting isn’t viable. This example uses short randomised strings, but random combinations of words or similar (e.g BlueTurkey, WalkingBread, VerbingNoun etc.) may be applicable in some scenarios as they are more easily able to be memorised.
Once the request ID is entered, the user shown the normal OAuth consent screen, where they can agree to share access to data or services with the third party application.
Upon authorising the application, this change is communicated back to the Smart TV application, which goes through the OAuth token exchange in order to obtain an access token and access data owned by the user.
This user authorisation does involve some additional steps compared to the standard 3-legged OAuth flow, as the user has to manually transfer the context for a request across channels via the request ID. However, in exchange, the user is assured that their credentials are never exposed to the third party, as they entirely manage their authentication while still being appropriately informed of the nature of the information that they are providing to the third party.
This flow extends the OAuth 3-legged flow with a number of additional services. Many of these services do not require application credentials or information about the user, and as such, could be blindly targeted by bad actors. Possible implications of this are outlined below.
Listening on the long-poll endpoint
As all that is required to register an interest in the long polling endpoint is a request ID, an actor could theoretically generate request IDs and listen on a very large number of potential IDs, some of which would eventually resolve, returning an authorisation code. While this authorisation code is able to be exchanged for an access token, doing so requires client application credentials, and so can be considered opaque, and useless to an attacker.
In addition, there may be multiple third party applications using this flow, all of which would share the map of request IDs. Assuming an implementation in which generated request IDs are independent of client_id (which they should be), for any given request ID, the resolved authorisation code could map to one of many applications. As such, even if an attacker possessed compromised client application credentials, they still may not be sufficient to exchange an authorisation code for a token (and an application attempting to exchange an authorisation code issued for a different application is a definite warning of potentially compromised client application credentials).
Using a malicious Client ID to generate requests
A bad actor who was able to register their own third party application could spam the /future_authorization endpoint with requests, to generate a huge number of request IDs, hoping that one resolves as the result of a user data entry error while entering a request ID for a different legitimate application authorisation request.
If the user then gave consent, they would give the bad actor’s application permission to access their data and services. This is a potential weakness, as it requires user vigilance to validate that they recognise the application and not simply blindly click ‘Yes’. The weakness can be mitigated somewhat by timing out authorisation requests periodically, to make it much more difficult for an application to occupy a significant proportion of the request ID space. An application with a large proportion of requests being timed out without being resolved is an indicator of potentially abusive behaviour.
This can also be mitigated somewhat by providing user consent management, such that they can revoke grants made to applications in error.
This flow is designed to supplement existing OAuth services, extending it with out-of-band services. It is also designed so it can be built around an OAuth implementation, with the out-of-band services running separately and calling into the OAuth implementation as required. In order to implement this latter mode, the out-of-band services need to provide a callback service, which overrides the callback URL provided by third party applications in the OAuth server. This is required so that the out-of-band services can capture the authorisation code which would normally be sent to the callback URL, and resolve the long-poll (or invoke the actual application callback URL).
The required services, including this callback service, are as follows:
- /future_authorization – Request for authorisation in future:
Requires client_id, scope (as normal for OAuth)
This service doesn’t do any authorisation yet, it just stores everything that is required in the hash map.
- /authorize – Resolve the authorisation request:
Requires request_id (from initial request)
On reception of the request_id, lookup the hashmap and assemble a call to the normal OAuth /authorize endpoint (which displays consent screens, as required)
This call to the OAuth endpoint requires:
client_id (from initial request)
redirect_uri (the callback uri provided by the out-of-band services, to capture the code)
scope (from initial request)
state – need to use this to resolve the callback (use request_id)
Responds with a 301 Redirect to the OAuth /authorize endpoint
- /callback – Callback URI
Hit following redirect. Uses the state variable to obtain the initial call from the hashmap, then resolve that call with the code.
Responds with a 301 Redirect back to a trusted interface page.
- /code – Long-poll endpoint
Implemented as a long poll endpoint, which resolves following hit on the callback URI for that request_id
I hope that this post has provided an option which is applicable to scenarios in which the standard OAuth 3-legged flows fall short. When requirements exist for third parties to utilise your services, and those third party applications don’t utilise a browser, performing all of the authentication and authorisation actions in a trusted channel is a viable option in order to ensure user credentials and data are protected, and those users have an assurance that this is happening.
In a future post, we will look at a concrete implementation of this flow, wrapping an existing OAuth server with additional services to provide non-browser based clients with the ability to leverage the consent flows provided by that server.
3 thoughts on “3-Legged OAuth for Non-Web Clients”
This content has come a long way after your initial scribble on the white board to explain – thanks and well done.