Built into the library, yada offers a complete standards-based set of security features for today’s secure applications and content delivery.
In yada, resources are self-contained and are individually protected from unauthorized access:
One design goal of HTTP is to separate resource identification from request semantics.
— RFC 7231 Section 2
Security is part of the semantics of a resource, so is part of resource itself, rather than coupled to the URI and routing. This approach improves the cohesion of the web resource, which can be tested independently.
As in all other areas, yada aims for 100% compliance with core HTTP standards when it comes to security, notably RFC 7235. Also, since HTTP APIs are nowadays used to facilitate transactional integration between systems via the user’s browser, it is important that yada fully supports systems that offer APIs to other applications, across origins, as standardised by CORS.
1 | First, the values of declared parameters (query, path, header, form
& body) are taken from the request and checked for validity. These
parameters may be used later, both in the identification of the user
and the resource being addressed. Invalid parameters would cause a
response with a 400 Bad Request status. |
2 | Next, the request is authenticated according to an authentication
scheme. This involves inspecting the request for claims about the
identity of the user (and verifying that these claims are genuine and
trustworthy). Unless there is something suspicious about the request’s
claims, such as the detection of a forgery attempt, no decision is
made at this stage about whether the request should be accepted or
rejected. |
3 | Next, the resource’s properties are determined, such as
existence, last modification time and, in particular, attributes
governing ownership and required access conditions. These may be
ascertained solely from the request or might involve one or more
requests to other sources, such as databases. |
4 | After this, an authorization step is carried out to determine
whether the credentials carried by the request, if any, are sufficient
to allow access to the resource. If not, a response is returned with a
401 or 403 status code—a 401 Unauthorized if no credentials are
present, and a 403 Forbidden if they are. A 401 gives the user-agent
the hint that it should attempt to capture authentication data from
the user and retry the request. |
5 | The request processing proceeds. Any response generated may depend
on information established by these steps. For example, certain
information might be filtered out of a response to requests that don’t
have sufficient authorization. |
Note that this design supports all of the following cases:
-
A resource is publicly accessible.
-
A resource is publicly accessible but is rendered differently for a
authenticated user.
-
A resource cannot be accessed without authentication.
-
A resource cannot be accessed without authentication and the user
having sufficient access rights.
In HTTP, authentication is the act of establishing the credentials of a user,
by checking the claims made in the Authorization
header of the request.
Authentication is achieved by declaring one (or more) authentication
schemes on the resource. An authentication scheme determines how the
request’s credentials are established. Credentials contain information
such as the user’s identity, roles and privileges, which can be used
to deny the request, or if approved, may affect the nature of the
response. IANA maintains a
registry
of HTTP authentication schemes, which include
Basic,
Digest,
Bearer,
HOBA and others.
Example 1. Protecting a yada resource with Basic Authentication
To declare a resource will be protected with Basic Authentication:
link:../dev/src/yada/dev/examples.clj[role=include]
1 | The resource contains an`:authentication` entry |
2 | The scheme is set to Basic |
3 | Any non-blank user is considered a success. Real-world cases would most likely check the password too. |
4 | A value is returned that will be bound as the context’s :credentials entry. |
5 | Optionally, a realm value can be specified. The support for, and semantics of a realm value depends on the authentication scheme. |
6 | The response to a GET request prints a string containing the user field. |
The :authenticate
function takes 3 arguments:
-
the yada context.
-
the credentials found in the value of the request’s Authorization header
. For some schemes, this is pre-processed for convenience. For example, in the case of Basic Authentication, the header is decoded into a vector containing the user and password sent by the user-agent.
-
the value of the authentication scheme, allowing for extra data to be specified on a per-resource basis.
The :authenticate
function MUST return one of the following:
-
A truthy value, indicating successful authentication, which will be bound to the yada context as the :credentials
entry.
-
Nil, indicating authentication has not be satisfied, for example, due to a bad password or illegal submission. No :credentials
entry is bound to the yada context.
-
The yada context, augmented as appropriate with a :credentials
entry.
-
Partial credentials, with a new authentication scheme to try (TBD)
Return values from the :authenticate
function MAY be
deferred
values. Since authentication often involves database or network calls it can be made asychronous to avoid blocking the request thread.
If a resource has multiple authentication schemes, use the
:authentication-schemes
entry instead, with a collection of auth
schemes.
{
:authentication-schemes
[{:scheme "Basic" …}
{:scheme "Digest" …}]
}
Challenges will be sent to the user-agent with each possible scheme,
allowing the user-agent to pick the best one.
Authorization is the act of allowing a user access to a resource.
This may require knowledge about the user only (for example, in
Role-based
access control) or may (additionally) depend on properties of the
resource identified by the HTTP request’s URI (as part of an
Attribute-based
access control authorization scheme). In either case, we assume that
the user has already been authenticated, and we are confident that
their credentials are genuine.
In yada, the resource’s properties are determined prior to the
authorization step, since it may be necessary to use these properties
in the authorization decision.
Authorization can be declared on a resource using an :authorization
entry:
{
:authorization
{:authorize (fn [ctx creds _]
…)
:custom/data 123}
}
The :authorize
function takes 3 arguments:
-
the yada context.
-
the :credentials
entry of the yada context — this can be established by an :authenticate
function or other means.
-
the authorization entry, which might contain extra declared data on a per-resource basis which may be used in determining the authorization.
The :authorize
function MUST return one of the following:
-
A truthy value, indicating successful authorization, which will be bound to the yada context as the :authorization
entry.
-
Nil, indicating access will not be granted to the resource. No :authorization
entry is bound to the yada context.
-
The yada context, augmented as appropriate with a :authorization
entry.
If no :authorize
function is specified then, by default, the following rules are applied:
-
If there are no authentication schemes declared on the resource, access is granted.
-
If there is at least one authentication scheme, and no credentials have been supplied, then access is denied.
If no extra data beyond the :authorize
function needs to be declared, then as a shorthand, the :authorize
function can be specifed at the top-level:
{
:authorize (fn [ctx creds] …)
}
Since there is no extra authorization data in this case, the function
only takes two arguments, since the third argument is not needed.
|
This section is currently being revised.
|
Basic Authentication has a number of weaknesses, such as the difficulty
of logging out and the lack of control that a website has over the
fields presented to a human. Therefore, the vast majority of websites
prefer to use a custom login form generated in HTML.
You can think of a login form as a resource that lets the user present
one set of credentials in order to acquire additional ones. The
credentials the user presents, via a form, are verified and if they are
true, a cookie is generated that certifies this. This cookie provides
the certification to subsequent requests in which it is sent.
Let’s start by building this login resource that will provide a login
form page to browsers and verify the form data when that form is
submitted.
Here’s a simplistic but viable resource model for the two methods
involved:
(require
'[buddy.sign.jwt :as jwt]
'[schema.core :as s]
'[hiccup.core :refer [html])
{:methods
{:post
{:consumes "application/x-www-form-urlencoded"
:parameters {:form
{:user s/Str :password s/Str}}
:response
(fn [ctx]
(let [{:keys [user password]} (get-in ctx [:parameters :form])]
(if (valid-user user password)
(assoc (:response ctx)
:cookies {"session"
{:value
(jwt/sign {:user user} "lp0fTc2JMtx8")}})
"Try again!")))}
:get
{:produces "text/html"
:response (html
[:form {:method :post}
[:input {:name "user" :type :text}]
[:input {:name "password" :type :password}]
[:input {:type :submit}]])}}}
The POST method method consumes incoming URL-encoded data (the classic
way a browser sends form data). It de-structures the two parameters
(user and password) from the form parameters.
We then determine if the user and password are valid (we don’t explain
here how this is done, but assume a valid-user
function exists that
can tell us). If the user is valid we associate a new cookie called
"session" with the response. By starting with the :response
value of
the request context, we ensure yada interprets our return value as a
Ring response rather than some other value.
We use Buddy’s sign
function to sign and encoded the cookie’s value as
a JSON string. We only specify the credentials as {:user user}
in this
case, but we could put much more into that map. The sign
function
requires us to provide a secret symmetric key that we can use for both
signing and verification, but the library does allow us asymmetric key
options too.
The other method, GET, simply produces a form for user-agents that can
render HTML (browsers, typically) to post back. For reasons of cohesion,
it’s a good idea to provide these two methods in the same resource to
encapsulate and dedupe the fields which are relevant to both the GET and
the POST.
The recommended way of logging out is to remove the session.
yada fully supports Cross-Origin Resource Sharing (CORS) allowing you to
provide APIs that are accessible from other origins.
For example, you may be creating an API that you wish other websites to
make use of, by allowing browsers visiting those websites access to your
API.
CORS is specified in the :access-control
section of the
resource-model.
{:access-control
{:allow-origin "*"
:allow-credentials false
:expose-headers #{"X-Custom"}
:allow-methods #{:get :post}
:allow-headers ["Api-Key"]
}}
With the exception of :allow-credentials
(which must be a boolean),
any of the values can be declared as single-arity functions, which are
called with the request-context as an argument to determine the value
for the corresponding response header.
clojure {:strict-transport-security {:max-age 12000}}
Defaults to a maximum age of 31536000.
The HSTS header is only set if the scheme is HTTPS or the service is
behind a proxy (determined by the presence of the X-Forwarded-For
request header).
{:content-security-policy "url-src"}
Defaults to default-src https: data: 'unsafe-inline' 'unsafe-eval'
.
A browser’s iframe can be used for 'click-jacking'. By default yada
tells browsers not to allow this. The default value is SAMEORIGIN
,
unless you override it in the resource-model.
{:x-frame-options "NONE"}
yada also sets the X-Xss-Protection
response header to
1; mode=block
. This can be overridden in the resource model.
{:x-content-type-options "0"}
By default, yada sets the X-Content-Type-Options
response header to
nosniff
. This tells browsers not to try to attempt to determine the
content-type of the response body.
Since yada sets the Content-Type
header according to HTTP standards,
there should never be a need for a browser to 'sniff' the response body
for this information, preventing an attack that might exploit some
vulnerability in this process.