This is the third part of my Authentication and security for REST API in the context of Web Apps series of posts. In the previous post, we discussed how to store and provide tokens to the server. In this post, we will try to design an API to authenticate users and manage tokens.
As mentioned in the introduction, the focus will be on a JSON-LD, Hydra and JWT-based implementation. However, this should be easily transposable to any REST implementation using an alternative for some or all of them.
Unfortunately, not all the Linked-Data vocabulary we will need is
already available. We will, sometimes, have to define our own vocabulary
terms. In these cases, we will use http://ld.lemoinem.name/ns/rest-auth# as
the base IRI for these new terms and rest-auth:
as their corresponding prefix.
These IRIs are not, currently, populated with linked-data description. In
particular, they are not HTTP dereferenceable, as of today. However, we intend
to provide such documents once the vocabulary has stabilized and is clear
enough.
This suggested API description is also intended to be presented as a JSON-LD/hydra:ApiDocumentation document describing the designed API at a later date.
Since the HATEOAS principle’s goal is discoverability, it is a given
that the login endpoint/operation IRI MUST be included in the public API
descriptions (such as a hydra:ApiDocumentation
referenced by a hydra:apiDocumentation
link).
The endpoint SHOULD be documented as a hydra:Resource
, with
an @type
appropriate for
a JWE JWT. This hydra:Resource
, or whatever is
used to represent the authentication endpoint in the API documentation, SHOULD
be given a wrds:describedby
property with
value rest-auth:authentication
.
This endpoint’s supported operations MUST include:
POST
operations on the resource itself, used to create a new
explicitly authenticated token,GET
operation, on the resource itself, used to refresh a token.Since tokens are intended to be opaque and stateless with respect to the server,
PUT
, PATCH
and DELETE
operations MUST NOT be supported. Token are
credentials and, therefore, intrinsically sensitive resources. They require
specific cache handling, which MUST be enforced by the server and respected by
any cache proxy:
Cache-control
Header with the private
directive,Cache-control
Header with either:
max-age
directive with a value less than the the token remaining lifetime,must-revalidate
directiveCache-control
Header with the s-maxage
directive.Vary
Header including “Authentication” (or any other
HTTP Header used to provide a token to the server) and “Cookie”.This restrictions are intended as a strict minimum. If an implementation chooses not to follow these recommendations, it MUST provide a stricter cache policy.
The restriction on the Vary
header is mostly important for unauthenticated or
anonymous resources. This one aside, the same restrictions SHOULD be applied on
any response including an access-restricted authenticated-clients-only resource.
To provide easier automation, responses to the token endpoint SHOULD present
a wrds:describedby
link with a
value rest-auth:authentication
.
An unauthenticated GET
operation (i.e. presented without a token or with an
expired token) on the token resource endpoint MUST return an anonymously
authenticated token.
An anonymously authenticated token is a token with a NULL sub
claim. All other
relevant claims MUST be filled as with any other token. In particular, exp
,
aud
and jti
are always relevant claims and MUST be filled for all
tokens. For reference, a NULL
claim MAY be removed from the token.
An authenticated GET
operation on the token resource endpoint, whose
authentication token has reached half its lifetime MUST send a renewed
token. The renewed token MUST have identical sub
, aud
and iss
claims. The
renewed token MUST have a level of authentication identical to or lower than the
original one (for reference, an explicitly authenticated token SHOULD be renewed
as a remember me token). This renewed token MUST have
a rest-auth:use-cookie
claim equivalent to the old
one. This renewed token MUST have a different jti
claim than the old one.
This renewed token’s jti
claim SHOULD be different from any other jti
claim
included in any previously issued token (and in particular, for the same
user). A cached token revalidation by the server MUST be consistent with this
process.
A POST
operation on the token resource endpoint MUST provide enough
information to start a credentials validation process. Once the process is
completed successfully, a fresh explicitly authenticated token MUST be returned
in the appropriate format (and transit mode). The response including this new
token SHOULD include a Content-Location
header containing the IRI of the token
endpoint itself. This MUST be considered a state modifying operation in the
context of CSRF protection to prevent Login CSRF attacks.
Once a user agent or a Web App has been through the authentication process and has been provided a new token, it SHOULD revalidate the hydra:ApiDocumentation resource as provided in the hydra:apiDocumentation link. This is to ensure API discoverability in the case where the Web App has been hiding its authenticated API.
The POST
operations on the token resources MUST support the following inputs
to achieve the following features. Each input SHOULD be tagged (in the API
documentations) with the appropriate value for
its wrds:describedby
property. These inputs MAY be
specified as query-string parameters.
rest-auth:use-cookie
: This input’s goal is to
generate a cookie stored token. This MUST NOT be used by an agent who support
more RESTful appropriate token storage technology. The token generated by this
authentication process MUST include an
encrypted rest-auth:use-cookie
claim set to true
.
It MUST be provided to the user agent through a cookie including the
httpOnly
flag. This cookie SHOULD include the secure
flag as well. If the
API supports non-HTTPS requests, which it SHOULD NOT, the secure
flag MAY be
omitted.rest-auth:remember-me
: This input’s goal is to
generate a long-term Remember Me token. The token generated by this
authentication process MUST NOT have an higher authenticated level than the
“Remember Me” level. The user agent SHOULD request a short-term token (which
SHOULD be marked as explicitly authenticated) as soon as possible after
receiving a long-term token.Since the token based authentication is stateless for the server, no logout endpoint is required. Logging out of the API simply means dropping the token and starting anew.
In order to prevent CSRF vulnerabilities, the API SHOULD reject with a
“401 Unauthorized” response any unauthenticated (i.e. not including any token)
state modifying request. The WWW-Authenticate
header MUST include a Bearer
challenge whose realm parameter MAY be set to the IRI of the token
endpoint.
Most services nowadays do not offer state modifying request to anonymous users (an exception could be a completely open and free platform such as Wikipedia).
In particular, the API MUST NOT generate an automatically managed out-of-band (e.g., in a cookie) non-anonymous authentication token in response to an unauthenticated (i.e. not including any token) request.
It is very important to consider that some authentication schemes (e.g., the ones relying on a trusted third party) could come with their own vulnerabilities to login CSRF even for manually managed authentication tokens.
The risk of login CSRF is greatly reduced for Web App using a manually managed token. However, the API must protect Web App using cookies (or another automatically managed out-of-band method) by ensuring they won’t issue an unrequested authentication token.
The WWW-Authenticate
header is not the preferred method of discoverability
because it is very loosely defined. User agents SHOULD rely on HATEOAS
discoverability (e.g. using Hydra) rather than the WWW-Authenticate
header until it is better integrated to the HATEOAS principle.
In the context of our API, a token is valid if:
nbf
claim, if
any,rest-auth:use-cookie
claim set
to true
MUST have been provided as an httpOnly
cookie. The cookie
MUST have the secure
flag unless the API allows non-HTTPS requests.rest-auth:use-cookie
claim or
with such a claim set to false
or NULL
MUST have been provided as a
Bearer
token through the Authentication
header.Additional properties of the token MAY be verified, such as CSRF
protection check or validating the credentials with the token’s issuer (when
there is a non-NULL
iss
claim).
In order to mitigate CSRF vulnerabilities, several checks SHOULD be done before accepting a token.
The protections against CSRF detailed here are based on the following assumptions:
“consistently publicize the origin” does not mean a user-agent must send a
Referrer
or Origin
Header to the server. It has complete and total freedom on
this point. However, it MUST apply this choice consistently during a single
session.
In particular, the API MUST include the user-agent’s Origin
as the value for
the aud
claim of any fresh token unless the user-agent’s Origin
is
NULL
. Renewed tokens MUST carry forward the aud
claim of their older token.
These CSRF checks only apply if the user-agent provides a token. Unauthenticated request cannot be protected against CSRF attacks.
CSRF protection is provided by matching the the user-agent’s Origin
with the the token’s Origin
.
CSRF protection MUST be double-checked for every state modifying request. CSRF protection SHOULD be double-checked for every authenticated request.
In order to protect browser user-agent, it is expected that user-agents not
requiring CSRF protection (such as a command-line API client), will not
provide any Origin
or will craft and consistently provide an appropriate
Origin
that couldn’t be generated by a browser user-agent.
This is not a protection against token theft since, an attacker able of stealing
a token will usually be able to determine the token’s original user-agent’s
Origin
easily.
Origin
First, the server MUST determine the user-agent’s Origin
using this algorithm:
Origin
Header in its request, the value of
this Header is the user-agent’s Origin
.Referee
Header in its request and the value of
this Header is an IRI, the Origin
’s components of its value (usually the
scheme, host and port parts of the IRI).Origin
is NULL
.Origin
The token’s Origin
is the value of its aud
claim.
For security and privacy reasons, an API MAY wish to hide its discoverable API to unauthenticated user agents.
In this case, any unauthenticated or anonymously authenticated request to the
API MUST provide a hydra:apiDocumentation
link to a
curated hydra:ApiDocumentation
. This unauthenticated
API Documentation MAY use the same IRI as the resource for the authenticated
one. In this case, the server MUST include a Vary
Header with “Authentication”
(or any other HTTP Header used to provide a token to the server) and “Cookie” in
the response.
This is to ensure caches will not be confused and the authenticated API remains discoverable easily.
If the authenticated and
unauthenticated hydra:ApiDocumentation
’s IRIs are
different, the authenticated API Documentation MUST include at least the same
information as the unauthenticated one. Moreover, any response to an
authenticated request MUST reference the authenticated ApiDocumentation.
Several different hydra:ApiDocumentation
resources
MAY be provided this way, each for a different level of authentication. However,
this might end up confusing both users and user agents. Therefore, API SHOULD
refrain from doing so.
In the next part, we will focus on adapting several user authentication schemes to this API.