-
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 Multi Factor Authentication (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 or multi factor credentials.
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 response with different potential 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. That Okta token is finally exchanged for a token generated by eAPD. The token generated by eAPD will contain all of the information necessary to determine what resources a user can access. This JWT is stored in local storage of the user's browser.
Mermaid Diagram here (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 application middleware that results in the user's full and trusted permission list appended to the request object. Verification of the token means checking that the signature is valid which prevents a user from tempering with the JWT payload (unless they have the JWT secret key). This prevents having to pull this information from the database on each request. When users make changes to their desired state they are reissued a token with the appropriate permissions for their new state.
The front end follows this flow chart for determining what state a user is in with respect to login
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_activities
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. This user object contains all of the relevant authorization information such as allowed activities and can be trusted because it comes directly from the JWT which is tamper proof
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 |