Security - wtsi-hgi/hgi-web GitHub Wiki
The token provider is an orthogonal service that generates security tokens, which are consumed by the service APIs for their authentication and authorisation needs. As such, there are a number of design constraints relevant to their generation:
-
Tokens need to serialise enough data to actually be useful. This must be done consistently and securely.
-
Tokens will be delegated to service APIs via the HTTP authorisation request header, using the
Bearer
method. (Thus, they must be representable in printable ASCII.) For the sake of backwards compatibility/graceful degradation, it would also be beneficial if the token can be broken into a pair for HTTP basic authentication. -
Tokens must have a limited lifetime. This too must, therefore, be encoded into itself.
-
In the context of the web client, the token provider cannot run upstream as it then becomes trivially accessible from its host (i.e., a massive security hole). It must run as a child process of the HTTP server.
The format of a basic authentication pair is the base64 encoding of the
pair, interposed with a colon (i.e., base64('username:password')
). We
extend this model by prepending data fields:
data1:...:dataN:expiration:salt:password
We can thus split out the password
field for basic authentication.
-
The data fields can be arbitrary, but should be semantically consistent. We will use the first field to contain a unique identifier for the user (e.g., uid, e-mail, etc.)
-
The expiration field will contain the expiry time, in seconds, from the Unix epoch.
-
The salt field will be a random string, to improve entropy conditions.
-
The password will be the HMAC of the other field data with a private key, using a proven hashing algorithm (e.g., SHA1), and base64 encoded. This (message, signature) pair can thus secure the token from deliberate modification or generation without access to said key.
The final token will thus be the base64 encoding of the above.
This system has been implemented as a standalone codec for Node.js, with decoders for other environments: Xiongxiong. The provider itself utilises this and is implemented to run under CGI.
The frontend client, its backend -- which functions mainly to amalgamate service APIs -- and the token provider will sit behind a Shibboleth gateway. The service APIs themselves will be accessible to the client server (i.e., routable from that host), but needn't be public facing (even behind Shibboleth). Indeed, it may be advisable to explicitly restrict access to them over the Internet.
Service APIs authenticate and authorise against a security token which, while in the context of the web client, is generated by the token provider from Shibboleth session data, is otherwise designed to be context-agnostic. This allows the service APIs to run in other contexts (e.g., a CLI wrapper) while maintaining security.
Once through the Shibboleth gateway, the end user will be forwarded to the client frontend. Our client will be designed to work with JavaScript disabled, so two routes are invoked in order to get and pass a token to the backend API (and thus the service APIs).
This is the easy route!
-
The client will load in the user's browser and ignore any redirections designed to handle non-JavaScript clients.
-
An XHR request will be made to the token provider to get the bearer token (e.g., in JSON). Once received, this will be managed by the application code in a suitable way.
-
Any subsequent XHR requests (i.e., to the backend, which are then delegated to service APIs) will have a
Bearer
authorisation header, with said token, injected into it. -
If an API call responds with a 401 error, we assume that the token has expired and negotiate the procurement of a new one (i.e., step two, above). Should this continue to fail, the client needs to bailout in a useful way.
Note that a Shibboleth session will expire after a set period of inactivity and/or a maximum duration. When this occurs, all requests made to a resource under Shibboleth protection will be redirected to the SSO on a different domain. Unfortunately, XHR requests transparently follow 302 codes, which will subsequently fail due to the single-origin policy. That is, we cannot detect when this happens programmatically.
There are some potential solutions:
-
Configure the reverse proxy to intercept 302 responses when they are requested from XHR (i.e., inspection of
X-Requested-With
) and rewrite them as 401 errors, which XHR can detect. -
Set the CORS header
Access-Control-Allow-Origin: *
on the Shibboleth IdP so that the XHR won't fail. The client can then detect that it has resolved to the SSO. -
Attempt to keep the Shibboleth session alive.
The first option is difficult to configure, but seems to be de facto. The CORS header solution is probably the most elegant -- despite the currently low coverage of browser support -- but requires cooperation from the IdP administrator.
Experimentally, we have shown that a Shibboleth session can be kept alive by making intermittent, nullipotent requests to a vacuous resource (a so-called "heartbeat" function). This method can keep the session alive until it reaches its maximum duration, which is considered long enough to be an edge case not worth worrying about.
-
The client will load in the user's browser where it will encounter a
<noscript>
tag: This is injected into the client HTML by the frontend server whenever basic autentication headers are missing or invalid. The<noscript>
tag contains a single<meta>
tag to redirect to the manual authentication flow. -
The manual authentication flow first starts by presenting the end user with instructions for what to do along with a token, presented as a basic authentication pair from the token provider (e.g., in an
<iframe>
). -
Once satisfied with the instructions, the user proceeds, where they are prompted by the browser for a basic authentication pair (i.e., that which they have just been shown).
-
Upon validation, they are forwarded back to the client page they started on, but with valid authentication headers, the
<noscript>
tag is omitted. -
Subsequent requests will all contain the basic authentication header, which will ultimately be delegated to the service APIs. When a token expires, the service APIs will respond with a 401 error, which will be interpreted by the client as a call to restart the manual authentication cycle.
Note that we cannot keep the Shibboleth session alive artificially, so a long period of inactivity will cause the IdP to bailout to the SSO. As our application is designed to be stateless, this is not a problem and, once the user has logged back in, they'll be returned to where they were kicked out.