No Access Token, No Service

Jared Hanson
Passport.js
Published in
4 min readSep 25, 2021

Version 1.6.1 of passport-oauth2 has been released. This version responds immediately with an HTTP error in situations when, upon exchanging the authorization code for an access token, the authorization server responds with a successful response, but that response is missing an access token.

This release was prompted by pull request #144, opened yesterday morning in conjunction with submitting a CVE (although I have not seen an actual record yet). Within 24 hours, I was contacted by the fine folks at Snyk requesting my assessment of the issue, and whether or not it constitutes a case of improper authentication (CWE-287).

In my assessment, this does not constitute such a vulnerability. Ordinarily, in cases of suspected security vulnerabilities, I would like time to thoroughly assess the details with the reporter and other security professionals. If deemed necessary, a response and public disclosure can then be coordinated. In the interest of transparency, and since the report is already public, I’m sharing the rationale for my assessment here.

OAuth 2.0 is a protocol that, among other things, allows authentication to a client (i.e., the application making use of passport-oauth2) to be delegated to an identity provider (IdP). When delegating in this way, the application is not actually authenticating users — the IdP is. Rather, the application is relying on the IdP to tell the application who it authenticated. Because of this reliance, clients are often referred to as relying parties (RPs) and they necessarily place a high degree of trust in the IdP.

One of the important aspects of a delegated authentication protocol is the mechanism by which the application can verify that statements made by the IdP are actually coming from the IdP itself, rather than an imposter. This is especially critical as these protocols often involve redirection through a user’s web browser, which is not a secure channel.

When using OAuth 2.0 to delegate authentication, the user is redirected to the IdP to authenticate. The IdP is responsible for authenticating the user, and then redirecting the user back to the application, along with an authorization code. This authorization code is then exchanged by the application for an access token by making an access token request directly to the IdP.

POST /token HTTP/1.1
Host: idp.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

This access token request is vital to the security of OAuth 2.0, as it is the first point at which the IdP can authenticate the application (via the Authorization header), and the application can authenticate the IdP (via TLS). Assuming the access token request is exchanging a valid authorization code issued to the application, the IdP responds with an access token response.

HTTP/1.1 200 OK
Content-Type: application/json

{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"Bearer",
"expires_in":3600,
}

At this point, the application is in possession of an access token that it knows was issued by the IdP. What if this successful response did not contain an access token, however? Would that bypass authentication?

The answer is “no”. At this point in the process, authentication is not complete. The application has obtained an access token, but it still has no idea who the IdP authenticated. In order to obtain that information, the application has to make another request to an endpoint at the IdP that will respond with information about the user who authenticated.

The exact details of this user information endpoint vary among providers, but this example of a request to and response from Facebook’s endpoint is illustrative:

GET /v12.0/me HTTP/1.1
Host: graph.facebook.com
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA
HTTP/1.1 200 OK
Content-Type: application/json

{
"id":"248289761001",
"name":"Jane Doe"
}

It is at this point, when the application makes a request asking the IdP to tell it who authenticated, that we obtain the necessary information to complete authentication. When using OAuth 2.0 for delegated authentication in this manner, the security model is relying on the IdP to properly authenticate this request and respond with the correct user information.

If the access token response were to not actually contain an access token (erroneously or otherwise), the subsequent user information request would lack credentials. At this point, the IdP should respond with an unauthorized error and the authentication process will fail.

It is, of course, possible that the IdP responds with a successful response, supplying user information to a request that lacks credentials. This would, indeed, be a serious security vulnerability — but it is a vulnerability that exists at the IdP. The application, having chosen to trust the IdP, has little recourse at this point beyond no longer relying on the IdP.

Versions of passport-oauth2 prior to 1.6.1 rely on the IdP to properly authenticate the user information request, including rejecting any requests that lack credentials. Versions from 1.6.1 and onward will continue to rely on the IdP to properly authenticate user information requests, since that is inherent in the protocol, but add an additional safe guard if an access token were to not be available.

While it is, of course, an improvement to have this safeguard as a form of defense in depth, I don’t believe it fundamentally alters the security model, and thus there is not an existing vulnerability prior to applying this patch.

That said, if concerns linger, the patch can be applied by updating dependencies:

npm update --depth=1

I consider transparency critical to security, so if there are any concerns or questions about my assessment, or if anyone feels I have overlooked important details, please contact me. If anything further needs to be disclosed, I will publish updates to this blog.

--

--