-
Notifications
You must be signed in to change notification settings - Fork 25
Development authentication and authorization
The application uses Okta to provide authentication. Users must register for an Okta account and be added to the application user group. Users must also sign up for MFA with Okta. They have the option of using voice call, email, sms text, or a software-based authenticator (such as Google Authenticator or Okta Authenticator) to provide one-time passwords.
Authentication is a multi-step process. The front-end uses the
@okta/okta-auth-js
node module to create an oktaClient object that is used to
interact with Okta. First, the oktaClient sign-in method sends the user's username
and password to Okta. Okta returns a transaction with different statuses. Error statuses
include LOCKED_OUT or PASSWORD_EXPIRED. The user is instructed on how to handle these situations.
If the user has not yet set up a multi-factor option, Okta will return a status of MFA_ENROLL. The user will then be walked through the steps to set up their second factor. If they choose to use email, Okta will use their EUA email. If they choose CALL or SMS, the user will have the option of supplying a phone number for Okta to use.
If the user has already set up a second factor, Okta will return a status of MFA_REQUIRED. The transaction includes an array of factors that the user has set up. That factor has a function named verify that will send the one-time password to the user when it is called. The user is then redirected to the page where they will enter the one-time password that they received from Okta.
Once the user enters the one-time password, oktaClient resumes the previous transaction, which has a verify function that takes the one-time password. If this one-time password is valid, Okta returns a transaction with the status 'SUCCESS' and a session token. The front-end then uses the oktaClient to get the access token using the session token and a state token generated by the front-end. Okta returns the same state token and the access tokens. If the state token matches with the one that was sent, the tokens are stored in local storage using oktaClient token manager.
(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 using the @okta/jwt-verifier
node
module. The JWT is sent to Okta and if valid, then the user's claims are returned.
Then the user is retrieved from Okta using the @okta/okta-sdk-nodejs
node module.
Permissions are then pulled from the database to create the full user object.
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. The roles map directly to groups in Okta.
Finally, there is auth_role_activity_mapping
that maps a role to a set of activities.
A user is assigned a role for a state. When a request arrives from the user, the JWT 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 |