-
Notifications
You must be signed in to change notification settings - Fork 25
Development authentication and authorization
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 containing the session ID.
The JWT is signed with the contents of the SESSION_SECRET
environment
variable (see the API configuration documentation
for more info on environment variables).
On subsequent calls to the API, the JWT is presented via the 'Authorization' header of each request. 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.
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.
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.
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 |