0.2.3 • Published 1 year ago

@byu-oit/okta-client v0.2.3

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
1 year ago

BYU logo @byu-oit/okta-client

Page Content

What is this Repo

This repo (published on npm) is an Okta version of the @byu-oit/hydra-client. It was created to easily interact with CES Okta by plugging certain modules into frontend/backend code. It was designed to specifically meet the needs of our internal application patterns.

If you're using this package you will likely want to be set up with github.com/byu-oit/ces-api-gateway as well.

Software CI: CES Okta Client Nodejs View ADRs: docs

Installation

npm install @byu-oit/okta-client

Examples

Registering an Application with CES Okta

Only members of the CES Identity team can register new applications with CES Okta. Typically, you will be registering one of two applications: a Frontend Application's BFF or a Backend REST Service. This section will give a general overview of what each application needs. Note that if your application is going to be in the Application Permission Mapping System you will need to request that the CES Identity team adds your application to the list.

Frontend Applications

A frontend typically consists of a frontend ("front channel") with a BFF ("back channel"). The BFF needs to have the following grant types available to it in CES Okta:

  • authorization_code - allows the BFF to sign a user in
  • client_credentials - used to get the token that identifies the BFF to other services
  • refresh_token - allows your application to request a new user access token using the provided refresh token (offline_access scope must be used to get the refresh token)

It will use its client credentials token to make calls to other REST Services behind CES Okta NOT the user's token received via the authorization code. Let the CES Identity team member that you're in contact with know that you need your application registered with those two grant types available. Since this is using authorization code, they will also ask you for a sign-in redirect URI (eg. localhost:8080/callback) and a sign-out redirect URI (eg. localhost:8080/signOutCompleteRoute).

Backend REST Services

A backend REST service only needs to have the client_credentials grant type enabled. Similar to the BFF, it will use the client credentials token to identify itself to other REST services it calls that are behind CES Okta.

Package Specific Details

Variables

The following variables are exported for your use:

  • CLIENT_MIDDLEWARE_COOKIE {string} - the name of the cookie that contains the user's information
  • CLIENT_MIDDLEWARE_TOKEN_COOKIE {string} - the name of the cookie that contains the token
  • EXPIRED_REFRESH_TOKEN_MESSAGE {string} - the message that will be sent in the response when a refresh token is expired and the UI needs to redirect the user to signin again

Functions

setup()

  • Should only be called ONCE when the server starts.
  • Sets variables the rest of the functions need (excluding clientMiddleware).
  • Gets a client credentials token and saves it
  • Must be included in all REST services. Can be used in BFFs.
  • Params:
    {
        wellknownUrl: String,
        clientId: String,
        clientSecret: String, // keep this value secure
        scope: String // optional
    }

request()

  • Used to make a client credentials call to another service registered in CES Okta
  • Uses the token retrieved in setup()
  • Gets a new token when the current one expires
  • Used by both BFFs & REST Services
  • Params:
    • The URL you are calling
    • whatever request options you want to include (uses node-fetch)
      • Example:

        {
            maxAttempts: 5 // defaults to 3 if this property is not included in requestOptions
        }

validateToken()

  • Available for those who would like more control/flexibility over validating a token
  • Uses the @okta/jwt-verifier package
  • Returns the decoded jwt OR throws an error
  • Consumer should handle all errors

apiTokenValidationMiddleware()

  • Used by REST Services
  • Sets up an express middleware to validate the token and the permissions
  • Any route set up after this middleware is used will go through the validation logic
  • Uses @okta/jwt-verifier
  • Returns 401 if invalid token
  • Returns 403 if invalid permissions

bffTokenValidationMiddleware()

  • Used by BFFs (backend for frontend)
  • Sets up an express middleware to validate the token (which comes from a request cookie) and the permissions
  • Any route set up after this middleware is used will go through the validation logic
  • Attempts to refresh the user's token if it has expired
  • If any error is thrown when trying to get a refresh token it returns status: 401 body: {message: 'expired_refresh_token'} for the FE to handle redirecting to /signin
    • Example of FE handling this error when an API call is made:
      if (!response.ok && response.status === 401) {
        const copy = response.clone() // Reading the body is a one-time operation, so checking if it equals the expected string means the body can't be read later. This is why we suggest to clone it.
        try {
          const json = await copy.json()
          if (json.message === EXPIRED_REFRESH_TOKEN_MESSAGE) { // have the user confirm they are okay losing any data
            if (window.confirm('Your session has expired. You need to log in again to continue. All entered data will be lost.')) {
              window.location.replace('/signin')
            } else { // return the response as if there was no error (fetch request was essentially canceled)
              return response
            }
          }
        } catch (err) {
          console.error('Unknown error while parsing response body.\n', err)
        }
      }
  • Uses @okta/jwt-verifier
  • Returns 401 if invalid token
  • Returns 403 if invalid permissions

setupAuthCodeRoutes()

  • Used by BFFs
  • Sets up express routes /signin /signout that redirect to the church's login
  • Gets the user's token using authorization code and sets it in a cookie named OAUTHTOKENCOOKIE
  • Params: (NOTE: the wellKnownUrl, clientId, & clientSecret do not need to be added IF the setup is called beforehand)
    {
        wellknownUrl: String,
        clientId: String,
        clientSecret: String, // keep this value secure
        scope: String, // typically 'openid profile offline_access CES_Apps'
        state: String, // optional 
        callbackUrl: String, // As registered with okta
        callbackRoute: String, // Middleware will create this route. Needs to match the callbackURL.
        signInSuccessRoute: String, // Your app needs to create this route. Middleware will redirect to it to return control back to the app after authorization succeeds.
        signInFailureRoute: String, // Your app needs to create this route. Middleware will redirect to it to return control back to the app after authorization fails.
        signOutCompleteRoute: String // Your app needs to create this route. Middleware will redirect to it to return control back to the app after signing out.
    }
  • Sets up an optional /validateToken route that is behind the apiTokenValidationMiddleware() and returns success if it makes it through the middleware

    • Example of the FE using this route to check that a user is signed in:

        if (cookie) {
        // check that the token cookie is still valid (refreshing it if needed)
        const response = await apiCall('/validateToken', {
          loadingMessage: 'Loading...',
          showToast: true
        })
      
        if (signedInCallback && response.ok) {
          console.log('signed in callback')
          const { initializeUser } = useUserInfoStore()
          await initializeUser(cookie)
          signedInCallback(cookie)
        } else if (response.canceled) {
          console.log('Signin canceled')
          // do nothing
        } else if (notSignedInCallback) {
          console.log('not signed in callback but has a cookie')
          notSignedInCallback()
        }
      } else if (notSignedInCallback) {
        console.log('not signed in callback')
        notSignedInCallback()
      }

permissionsValidator()

  • Used by APIs
  • Checks the client's permission to access a route
  • Should only be used when an API route requires permissions
  • Parameters:
    • req: Request object from an Express Request
    • permissions: Array<string>
  • Returns:
    • true when JWT has sufficient permissions
    • false when JWT does not have sufficient permissions
  • Example
app.get('/pokemon', (req, res) => {
    const permissions = ['BYU.Finance.SpendMoney']
    const hasPermission = await okta.permissionsValidator(req, permissions)
    if (!hasPermission) res.status(403).send({message: 'Invalid permissions'})
    "... SNIPPET"
})

Testing Locally

The example files are useful for testing locally. Run the client and the service on different ports in accordance with the applications used for testing (must be registered in CES Okta).

How to get CES UUID in the JWT claims

The CES UUID is not included in the JWT by default. To get the CES UUID as a claim in the JWT do one of the following: 1. Include the CES_Apps scope 2. Send no scopes (CES_Apps is a default scope and will be added for you)

Okta JWT Overview

If you are unfamiliar with JWTs, review A Beginner's Guide to JWTs.

Okta tokens can also be referred to as JWTs. When using this package it is good to be familiar with two types of JWTs: 1. User Access Tokens (used for frontend) 2. Client Credentials Tokens (used for backend)

User Access Tokens

These tokens are given to a frontend client when a user successfully logs in through the authorization code flow.

Here is an example of what is inside the JWT body

{
  ver: 1,
  jti: 'a token id',
  iss: 'the token issuer',
  aud: 'audience',
  iat: 1677777749,
  exp: 1677781349,
  cid: 'the client id',
  uid: 'your user id',
  scp: [ 'openid', 'profile', 'CES_Apps' ],
  auth_time: 1677777748,
  sub: 'your email address',
  cesUUID: 'your ces uuid' // only with CES_Apps scope 

}

The current standard is storing this as a cookie for the frontend to use.

What are the claims of this token?

ver - the token version

jti - the token id

iss - who issued the token

aud - the intended receiver of the token

iat - timestamp of when the token was issued

exp - timestamp of when the token will expire

cid - the client id of the application that requested the token (This is a claim Okta adds)

uid - the user id who requested the token (This is a claim Okta adds)

scp - the OAuth 2.0 permissions requested

auth_time - timestamp of when the user successfully logged in

sub - who requested the token

cesUUID - the CES UUID of the user who requested the token (see How to get CES UUID in the JWT claims)

Client Credentials Access Tokens

These tokens are given to a backend client when it requests a token via the client credentials flow.

Here is an example of what is inside the JWT body

{
  ver: 1,
  jti: "a token id",
  iss: "the token issuer",
  aud: "audience",
  iat: 1677777749,
  exp: 1677781349,
  cid: "the client id",
  scp: [],
  auth_time: 1677777748,
  sub: "the client id",
  APMS_permissions: ['ces.testing.all_access']        
}

NOTE: To have the APMS_permissions claim in the JWT, you will need to add the APMS scope.

What are the claims of this token?

ver - the token version

jti - the token id

iss - who issued the token

aud - the intended receiver of the token

iat - timestamp of when the token was issued

exp - timestamp of when the token will expire

cid - the client id of the application that requested the token (This is a claim Okta adds)

scp - the OAuth 2.0 permissions requested

auth_time - timestamp of when the user successfully logged in

sub - the client id that requested the token

APMS_permissions - The permissions the client is granted

OAuth Overview

This section will give a general overview of the pattern we are using in our application interactions.

User Login Flow using CES Okta The above diagram outlines the basic pattern of a SPA (Single Page Application) that uses a BFF (Backend For Frontend). This package serves as a middle man for all interactions with CES Okta so the developer of the SPA/BFF doesn't need to worry about CES Okta specific details. The following functions in this package will be useful in this flow (see the Client example): clientMiddleware

BFF External Call Flow using CES Okta The above diagram outlines the basic pattern of a BFF interacting with an external REST Service. This same pattern applies to any REST service interacting with another REST service (simply remove the frontend and consider the BFF as another REST service). This package serves as a middle man for all interactions with CES Okta so the developer of the BFF or REST Service doesn't need to worry about CES Okta specific details. The following functions in this package will be useful in this flow (see the Service example for REST Services & the Client example for BFFs): setup, serviceMiddleware (only for REST Services), request

More Info

If you want to understand the OAuth concepts better, consider these resources:

0.2.3

1 year ago

0.2.2

1 year ago

0.2.1

1 year ago

0.2.0

1 year ago

0.1.9

1 year ago

0.0.10

1 year ago

0.0.11

1 year ago

0.0.12

1 year ago

0.0.13

1 year ago

0.0.14

1 year ago

0.1.0

1 year ago

0.1.2

1 year ago

0.1.1

1 year ago

0.1.8

1 year ago

0.1.7

1 year ago

0.1.4

1 year ago

0.1.3

1 year ago

0.1.6

1 year ago

0.1.5

1 year ago

0.0.9

1 year ago

0.0.8

1 year ago

0.0.5

1 year ago

0.0.4

1 year ago

0.0.7

1 year ago

0.0.6

1 year ago

0.0.3

1 year ago

0.0.2

1 year ago

0.0.1

1 year ago

2.3.0

1 year ago