REST API and Authentication

TLDR; just see the Recommendations section below.

On the Platform Team call this week, @ruhanga pointed out that the REST API returns two different error codes on authentication failure, a 401 if the authentication failed and a 403 if the session was expired. In both these cases, the client needs to reauthenticate with the server. This behaviour was explicitly added to the REST module due to REST-277, which was looking for a way to distinguish between a 401 for “authentication failed” and a 401 for “session expired”.

Some History

A distinction between HTTP response codes has existed since at least HTTP 1.0 (RFC 1945), but 401 responses were used in a particular flow for HTTP BASIC authentication. Essentially, the browser would send a request to the server and if the server decided that a resource need authentication it would respond with an HTTP 401 response and an appropriate challenge in the WWW-Authenticate header. The browser would then collect credentials from the client and respond to the challenge. If the server decided the credentials were wrong or the credentials provided did not grant the user access to the resource, it would respond with a second HTTP 401 response, which effectively killed the login. (Basically, this second response is the “bad username or password” response).

HTTP 403 responses existed completely outside of the above described flow. I.e., a 403 (Forbidden) response “indicates that the server understood the request but refuses to authorize it” (from RFC-7231).

Existing Practices

In addition to considering how the HTTP specs themselves have involved it’s useful to consider how APIs in general actually work.

OAuth 2.0

OAuth 2.0 is a specification designed to create a secure way for browser-based applications to communicate authorization information to an API server. OAuth 2.0 makes use of the following HTTP codes when dealing with authorization failures (taken from RFC 6750):

HTTP 400: if the authorization request is malformed. Among other things, this would include where the token was request (via the 401 flow above), but the token wasn’t sent. HTTP 401: The access token provided is expired, revoked, malformed, or invalid for other reasons. HTTP 403: The request requires higher privileges than provided by the access token.

REST API Design Rulebook

The book REST API Design Rulebook from O’Reilly gives these simple rules:

  • “401 (“Unauthorized”) must be used when there is a problem with the client’s credentials”
  • “403 (“Forbidden”) should be used to forbid access regardless of authorization state”

Current OpenMRS API Clients

Currently, most OpenMRS API clients seem to rely on the availability of cookies to authenticate with the REST API; the exception I can find are the react-components library, which appears to use pre-emptive Basic authentication.

Conclusions and Recommendations

It seems that the behaviour requested in REST-277 results in us using an HTTP status code in a very non-standard way. In particular, I cannot find another instance where a 403 status code is used to mean “retry authentication”; the 401 status code is exclusively used for this and 403 is used exclusively for cases where the auth flow is terminated due to an authorization (rather than authentication) error. Thus, we should revert to returning a 401 status code regardless of whether the action failed due to an expired cookie or invalid authentication credentials.

The request in REST-277, however, seems reasonable. An API client may need to distinguish between “the server rejected this request, but you can try to authenticate again” and “the server rejected this request” and the various RFCs provide us with a mechanism to do that via including or not including an WWW-Authenticate header in the response. Since we support two authentication schemes, cookie-based authentication (which seems to be widely used) and Basic authentication we should use this header to indicate this.

Recommendations

I would suggest we adopt the following flow:

  1. Request hits the API, we check whether there is a current valid session (Cookie-based authentication). If there is, the request will be processed.
  2. If there isn’t a currently valid session, we check for a Basic authentication header.
  3. If there isn’t a Basic authentication header, we return an HTTP 401 error with the WWW-Authenticate header set to Basic, OpenMRS-Cookie.
  4. If there is a Basic authentication header, we attempt to login with those credentials. If the login succeeds, the request is processsed as normal.
  5. If the login fails at this point we return an HTTP 401 error with the WWW-Authenticate header set to OpenMRS-Cookie.

This flow should allow:

  1. The use of cookie-based authentication where a valid session already exists
  2. The use of pre-emptive Basic authentication
  3. The use of the standard Basic authentication flow (where a request is made and a 401 response with the WWW-Authenticate header is returned, followed by a request with Basic authentication).
  4. The client to surmise that the cookie they are sending is invalid.

I also don’t think this unnecessarily reveals any information that a hacker would not otherwise be able to infer. The downside is that a mis-behaved API client might not read the WWW-Authenticate header and take it’s presence to mean “continue attempting to authenticate”, but that’s probably outside the scope of this.

Thoughts? Comments? Corrections? Amendations?

7 Likes

Amazing , My appreciation , thanks for for the resource

Thanks so much @ibacher for outlining this so clearly + :100: So helpful.

My shortcut: there should never have been a 403, rather two variations on 401.

If I’m correct, can we open a corrective ticket for this, or are we worried that people out there have been relying on this 403?

Excellent summary, @ibacher. I agree with the proposal.

Is there a reason to call the challenge OpenMRS-Cookie instead of simply Cookie?

I’m fully intending to write-up a ticket today. This post is mostly to explain the reasoning and provide something we can link to in case anyone is relying on the current 403 behaviour.

And yes, your shortcut is correct.

Really it’s just a matter of caution. E.g., adding WWW-Authenticate: Basic will cause browsers to prompt the user for a username and password if they try to navigate to the API directly. I was just coming up with a name that was less likely to be used and specific to OpenMRS since getting a cookie as a sort of token is common, but how you get that cookie is generally application-specific. That way there aren’t any unexpected pieces trying to handle it.

Ticketed as RESTWS-859.

3 Likes