bcgov/citz-imb-staff-purchasing-reimbursement

View on GitHub
api/keycloak/README.md

Summary

Maintainability
Test Coverage
# BCGov SSO Keycloak Integration

For NodeJS:18 Express API

<br />

<img src="https://user-images.githubusercontent.com/16313579/223932048-a254cbfd-aa7e-43ef-ae66-c4f9c360bf06.png">

## Table of Contents

- [General Information](#general-information)
- [Directory Structure](#directory-structure)
- [Getting Started with the Integration](#getting-started-with-the-integration)
- [Environment Variables](#environment-variables)
- [Authentication Flow](#authentication-flow)
- [Authentication on an Endpoint](#authentication-on-an-endpoint)
- [Authorization on an Endpoint](#authorization-on-an-endpoint)

## General Information

- For running on a NodeJS:18 Express API.
- For Keycloak Gold Standard.

## Directory Structure

```JavaScript
keycloak/
|─ configuration.js
|  └─ // Imports environment variables and exports configuration variables.
|  └─ // See (Environment Variables) section for required variables.
|
|─ controllers.js
|  └─ // Functions that process the request of an endpoint.
|
|─ index.js
|  └─ // Exports middleware and keycloakInit functions.
|  └─ // keycloakInit is responsible for initializing the Keycloak integration with the API.
|
|─ middleware.js
|  └─ // Creates the middleware that is exported by the index.js file.
|  └─ // Middleware protects an endpoint so that it requires the user be logged in to use.
|  └─ // Middleware provides user object on the request which is the decoded user info from Keycloak.
|
|─ routes.js
|  └─ // Routes used to authenticate with the Keycloak integration.
|
|─ utils.js
|  └─ // Functions used by the Keycloak integration.
```

<br/>

## Getting Started with the Integration

1. Place `keycloak/` directory in the src directory.
2. Add import `const { keycloakInit } = require("./src/keycloak");` to the top of the file that defines the express app.
3. Add `keycloakInit(app);` below the definition of the express app.
4. Add the required packages with `npm i express axios body-parser dotenv cookie-parser` and `npm i -D @types/express @types/cookie-parser`.
5. Add the required environment variables from the (Environment Variables) section below.

<br />

## Environment Variables

```ENV
# Ensure the following environment variables are defined on the container.

ENVIRONMENT= # (local only) Set to 'local' when running container locally.
FRONTEND_PORT= # (local only) Port of the frontend application.
API_PORT= # (local only) Port of the backend application.

FRONTEND_URL= # (production only) URL of the frontend application.
BACKEND_URL= # (production only) URL of the backend application.

SSO_CLIENT_ID= # Keycloak client_id
SSO_CLIENT_SECRET= # Keycloak client_secret
SSO_AUTH_SERVER_URL= # Keycloak auth URL, see example below.
# https://dev.loginproxy.gov.bc.ca/auth/realms/standard/protocol/openid-connect
```

<br />

## Authentication Flow

The Keycloak Authentication system begins when the user visits the `/oauth/login` endpoint.

1. The user is redirected to the Keycloak Login Page for our application.
2. Upon successful login, they are redirected back to our app's `/oauth/login/callback` endpoint with an "authentication code".
3. Using this authentication code, our api reaches out to Keycloak on our user's behalf and retrieves them an [access token], and a [refresh token].
4. The user is redirected back to the frontend with an access token within the query parameters, and a refresh token as an httpOnly cookie.

<br/>

## Authentication on an Endpoint

Require keycloak authentication before using an endpoint.
Import `middleware` from `keycloak` and add as middleware.

Example (middleware is aliased to 'protect'):

```JavaScript
const { middleware: protect } = require("./keycloak");

app.use("/users", protect, usersRouter);
```

<br/>

## Authorization on an Endpoint

Get the keycloak user info in a protected endpoint.  
`req.user` is either populated or null and the `client_roles` property is either a populated array or undefined.

Example:

```JavaScript
const user = req.user;
if (!user) res.status(404).send("User not found.");
else // Do something with user.
```

Example req.user object:

```JSON
{
  "idir_user_guid": "W7802F34D2390EFA9E7JK15923770279",
  "identity_provider": "idir",
  "idir_username": "JOHNDOE",
  "name": "Doe, John CITZ:EX",
  "preferred_username": "a7254c34i2755fea9e7ed15918356158@idir",
  "given_name": "John",
  "display_name": "Doe, John CITZ:EX",
  "family_name": "Doe",
  "email": "john.doe@gov.bc.ca",
  "client_roles": ["admin"]
}
```

<!-- Link References -->

[access token]: https://auth0.com/docs/secure/tokens/access-tokens
[refresh token]: https://developer.okta.com/docs/guides/refresh-tokens/main/