Skip to content

Development authentication and authorization

Greg Walker edited this page Jan 13, 2020 · 25 revisions

Authentication

The API uses Passport to simplify authentication. Currently, we only support authentication against a local database. Users are stored in a users table, uniquely identified by an email address, with a PBKDF2-hashed password (including a random salt and a number of iterations chosen to take about 300 milliseconds on the target infrastructure).

Authentication is a two-step process. First, users obtain a nonce based on their username (usernames are users' email addresses). This nonce is cryptographically signed by the server and expires 3 seconds after it is issued. To get a nonce, send an HTTP POST request to the /auth/login/nonce endpoint. The body of the request must be a JSON object containing a username property. The response will be a JSON object containing a nonce property.

NOTE: Nonces are issued for any request containing a username property. Obtaining a nonce does not indicate a valid username.

The second step is to send an HTTP POST request to the /auth/login endpoint. The body of the request must be form-encoded or JSON, and contain username and password fields. The username field must be the nonce retrieved earlier. The API response is primarily just a status code: 200 for a successful login, 400 for invalid request, 401 for invalid login, or 500 for a server error. There is no response body for successful logins or server errors.

On a successful login, an authentication token is generated and stored in its entirety in a cryptographically-signed JWT cookie containing the session ID. The cookie is signed with the contents of the SESSION_SECRET environment variable (see the API configuration documentation for more info on environment variables).

authentication flow diagram

On subsequent calls to the API, the token is verified by checking the signature and expiration date. Then the session ID is extracted from the token, and the database is queried to find a user ID that matches the session ID (also checks that the session is not expired in the database). That user ID is then used to create a full user object from the database.

authentication token verification diagram

Authorization

We're using a combination of role-based and activity-based authorization: the actual logic of authorizing requests is purely activity-based, but to simplify administration, we're using roles to group activities. A user has a role, and that role identifies what activities they have permission to perform.

Our list of roles and activities is available and updated as new ones are introduced.

How it works technically

Data model

There are three tables to support this authorization model. auth_activites is the full list of activities known to the system. auth_roles is, likewise, the full list of rules. Finally, there is auth_role_activity_mapping that maps a role to a set of activities.

The server

A user is assigned a role. When a request arrives from the user, the session cookie is decrypted and a user object is created, as described in the authentication documentation. During that user creation step, the user's role is fetched from the database, and that role is then mapped to a list of activities. Finally, the list of activities is attached directly to the user object.

From there, we have an authorization middleware called can. Each endpoint can register that it needs authorization by adding a call to the can endpoint during its setup, along with the activity the user needs in order to be authorized:

express.get('/my/path', can('dance-like-nobody-is-watching'), function(req, res, next) { ...

The can middleware will first use the loggedIn middleware to make sure the user is logged in, and then it will verify that the user object has the requested activity.

case result
not logged in an HTTP 403 status is sent, and the endpoint handler is never called
logged in, does not have permission an HTTP 401 status is sent, and the endpoint handler is never called
logged in, has permission the endpoint handler is called
Clone this wiki locally