-
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.
Authentication is a multi-step process. The front-end uses the @okta/okta-auth-js node module to create an oktaClient that is used to interact with Okta. First, oktaClient.signIn sends the user's username and password to Okta. Okta returns a transaction with the status 'MFA_REQUIRED'. This transaction also includes a array of factors that will contain a factor with a provider 'OKTA'. 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.tx.resume is used to continue 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 calls oktaClient.token.getWithoutPrompt 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.tokenManager.add('accessToken', accessToken).
!
(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. 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 |