0.0.1 • Published 1 year ago

@bcgov/citz-imb-kc-express v0.0.1

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

BCGov SSO Keycloak Integration for Express

Lifecycle:Experimental License

NPM NodeJS Typescript Express

  1. Install package by following the steps at Installing the Package.
  2. Set up the package by following the steps at Basic Setup Guide.
  3. For use with @bcgov/citz-imb-kc-react.

Table of Contents

General Information

  • For running on a NodeJS:20 Express API.
  • For Keycloak Gold Standard.
  • Works with Vanilla JavaScript or Typescript 5.
  • For use with @bcgov/citz-imb-kc-react

Installing the Package

Run npm install @bcgov/citz-imb-kc-express or select a specific version tag from NPM Package.

Return to Top

Basic Setup Guide

  1. Add import const { keycloak } = require('@bcgov/citz-imb-kc-express'); or import { keycloak } from '@bcgov/citz-imb-kc-express'; to the top of the file that defines the express app. Add keycloak(app); below the definition of the express app, where app is defined by express().

Example:

import express, { Application } from 'express';
import { keycloak } from '@bcgov/citz-imb-kc-express';

// Define Express App
const app = express();

// Initialize Keycloak(app: Application, options?: KCOptions).
keycloak(app);
  1. Add the required environment variables from the Environment Variables section below.

Return to Top

Environment Variables

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

FRONTEND_URL= # URL of the frontend application.
BACKEND_URL= # 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

DEBUG= # (optional) Set to 'true' to get useful debug statements in api console.
VERBOSE_DEBUG= # (optional) Set to 'true' to get extra details from DEBUG.
SM_LOGOUT_URL= # (optional) Site minder logout url, see default value below.
# https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi

Return to Top

Directory Structure

.
├── .github/
|   ├── config/
|   |   └── dep-report.json5                # Configure options for NPM Dep Report.
|   ├── helpers/
|   |   ├── github-api/                     # Functions to access the GitHub API.
|   |   ├── create-npm-dep-report-issues.js # Creates GitHub Issues for Npm Dep Reports.
|   |   ├── create-npm-dep-report.js        # Creates text bodies for Npm Dep Reports.
|   |   ├── parse-json5-config.js           # Parses json5 files for GitHub actions output.
|   |   └── parse-npm-deps.js               # Parses package.json files for changes to package versions.
|   ├── workflows/
|   |   ├── npm-dep-report.yaml             # Reports on new package versions.
|   |   └── releases.yaml                   # Creates a new GitHub Release.
├── .husky/
|   └── post-commit                         # Script that runs after a git commit.
├── scripts/
|   ├── bump-version.mjs                    # Bumps version in package.json file.
|   ├── post-commit-version-change.mjs      # Bumps version when post-commit is run.
|   ├── remove-dts-files.mjs                # Removes TypeScript declaration files from the build.
|   └── remove-empty-dirs.mjs               # Removes empty directories from the build.
├── src/                                    # Source code for package.
|   ├── utils/                              # Utility functions.
|   ├── config.ts                           # Config variables.
|   ├── controllers.ts                      # Controllers such as login and logout.
|   ├── index.ts                            # Export functions for the package.
|   ├── middleware.ts                       # Protected route middleware.
|   ├── router.ts                           # Router for routes such as login and token.
|   └── types.ts                            # TypeScript types.
├── package.json                            # Package config and dependencies.
├── .npmrc                                  # NPM config.
├── rollup.config.mjs                       # Builds and compiles TypeScript files into JavaScript.
├── rollupdts.config.mjs                    # Builds and compiles TypeScript declartion files.

Return to Top

Scripts

# Compile all src code into a bundle in build/ directory.
$ npm run build
# Part of 'build' and it bundles the typescipt declarations into a single bundle.d.ts file.
$ npm run build:dts
# Part of build and it removes directories and files before the build.
$ npm run clean:prebuild
# Part of build and it removes directories and files after the build.
$ npm run clean:postbuild
# Used to package the code before a release.
$ npm run pack

Return to Top

Module Exports

These are the functions and types exported by the @bcgov/citz-imb-kc-express module.

import {
  keycloak, // Initializes the keycloak service in your express app.
  protectedRoute, // Middleware function used for authentication and authorization.
  hasRoles, // Utility function used to return a boolean if user has specified roles.
} from '@bcgov/citz-imb-kc-express';

// TypeScript Types:
import {
  KeycloakUser, // Base type for req.user
  KeycloakIdirUser, // User types specific to Idir users.
  KeycloakBCeIDUser, // User types specific to BCeID users.
  KeycloakGithubUser, // User types specific to Github users.
  KCOptions, // Type of optional second parameter for keycloak()
  ProtectedRouteOptions, // Type of optional second parameter for protectedRoute()
  HasRolesOptions, // Type of optional third parameter for hasRoles()
  IdentityProvider, // Combined type for identity providers.
  IdirIdentityProvider, // Used for more efficient login.
  BceidIdentityProvider, // Used for more efficient login.
  GithubIdentityProvider, // Used for more efficient login.
} from '@bcgov/citz-imb-kc-express';

Return to Top

TypeScript Types

These are the TypeScript types of the @bcgov/citz-imb-kc-express module.

const keycloak: (app: Application, options?: KCOptions) => void;
const protectedRoute: (roles?: string[], options?: ProtectedRouteOptions) => RequestHandler;
const hasRoles = (
  user: KeycloakUser,
  roles: string[],
  options?: HasRolesOptions
) => boolean;

export type IdirIdentityProvider = "idir";
export type BceidIdentityProvider =
  | "bceidbasic"
  | "bceidbusiness"
  | "bceidboth";
export type GithubIdentityProvider = "githubbcgov" | "githubpublic";

export type IdentityProvider = IdirIdentityProvider &
  BceidIdentityProvider &
  GithubIdentityProvider;

export type BaseKeycloakUser = {
  name?: string;
  preferred_username: string;
  email: string;
  display_name: string;
  client_roles?: string[];
  scope?: string;
  identity_provider:
    | IdirIdentityProvider
    | BceidIdentityProvider
    | GithubIdentityProvider;
};

export type KeycloakIdirUser = {
  idir_user_guid?: string;
  idir_username?: string;
  given_name?: string;
  family_name?: string;
};

export type KeycloakBCeIDUser = {
  bceid_user_guid?: string;
  bceid_username?: string;
  bceid_business_name?: string;
};

export type KeycloakGithubUser = {
  github_id?: string;
  github_username?: string;
  orgs?: string;
  given_name?: string;
  family_name?: string;
  first_name?: string;
  last_name?: string;
};

export type KeycloakUser = BaseKeycloakUser &
  KeycloakIdirUser &
  KeycloakBCeIDUser &
  KeycloakGithubUser;

export type KCOptions = {
  afterUserLogin?: (userInfo: KeycloakUser) => Promise<void> | void;
  afterUserLogout?: (userInfo: KeycloakUser) => Promise<void> | void;
};

export type ProtectedRouteOptions = {
  requireAllRoles?: boolean;
};

export type HasRolesOptions = {
  requireAllRoles?: boolean;
};

Return to Top

Initialization Options

Optional second parameter to the keycloak() function.
Use cases may include adding user to database upon first login or updating a last login field.

Example:

import { KCOptions, KeycloakUser, keycloak } from "@bcgov/citz-imb-kc-express";

const KEYCLOAK_OPTIONS: KCOptions = {
  afterUserLogin: (user: KeycloakUser) => {
    if (user) activateUser(user);
  },
  afterUserLogout: (user: KeycloakUser) => {
    console.log(`${user?.display_name ?? "Unknown"} has logged out.`);
  },
};

// Initialize keycloak:
keycloak(app, KEYCLOAK_OPTIONS);

Return to Top

Authentication on an Endpoint

Require keycloak authentication before using an endpoint. Import protectedRoute from @bcgov/citz-imb-kc-express and add as middleware.

import { protectedRoute } from '@bcgov/citz-imb-kc-express';

// Use in file where express app is defined:
app.use("/users", protectedRoute(), usersRouter);

// OR use in router:
import express from "express";
const router = express.Router();

router.get("/", exampleController());
router.get("/protected", protectedRoute(), exampleProtectedController());

!Important
The following WILL NOT WORK:

import { protectedRoute } from '@bcgov/citz-imb-kc-express';

// THIS WILL NOT WORK.
app.use("/api", protectedRoute(), guestRouter);
app.use("/api", protectedRoute(['admin']), adminRouter);

Return to Top

Authorization on an Endpoint

The first way of authorization on an endpoint is by adding the required roles to the protectedRoute() middleware that was introduced above.

// Users must have 'Member' role.
app.use("/post", protectedRoute(['Member']), postRouter);

// Users must have BOTH 'Member' and 'Commenter' roles.
// requireAllRoles option is true by default.
app.use("/comment", protectedRoute(['Member', 'Commenter']), commentRouter);

// Users must have EITHER 'Member' or 'Verified' role.
app.use("/vote", protectedRoute(['Member', 'Verified'], { requireAllRoles: false }), voteRouter);

!Important
The following WILL NOT WORK (see above section for router usage and workaround):

import { protectedRoute } from '@bcgov/citz-imb-kc-express';

// THIS WILL NOT WORK.
app.use("/api", protectedRoute(), guestRouter);
app.use("/api", protectedRoute(['admin']), adminRouter);

Here is how to get the keycloak user info in a protected endpoint.

!IMPORTANT req.user.client_roles property is either a populated array or undefined. It is recommended to use the hasRoles() function instead of checking req.user.client_roles.

Example within a controller of a protected route:

Add import import { hasRoles } from '@bcgov/citz-imb-kc-express' if using hasRoles() function.

Use case could be first protecting the route so only users with the Admin, Member, Commenter, OR Verified roles can use the endpoint, and then doing something different based on role/permission.

const user = req?.user;

// Do something with users full name.
const userFullname = `${user.given_name} ${user.family_name}`;

// User must have 'Admin' role.
if (hasRoles(user, ['Admin'])) // Do something...

// Users must have BOTH 'Member' and 'Commenter' roles.
// requireAllRoles option is true by default.
if (hasRoles(user, ['Member', 'Commenter'])) // Do something...

// Users must have EITHER 'Member' or 'Verified' role.
if (hasRoles(user, ['Member', 'Verified'], { requireAllRoles: false })) // Do Something...

For all user properties reference SSO Keycloak Wiki - Identity Provider Attribute Mapping.
Example IDIR req.user object (Typescript Type is KeycloakUser & KeycloakIdirUser):

{
  "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"]
}

Return to Top

Authentication Flow

Return to Top

Applications using Keycloak Solution

The following applications are currently using this keycloak implementation solution:

SET - Salary Estimation Tool PIMS - Property Inventory Management System (experimenting in express-api directory) PLAY - CITZ IMB Package Testing App

Return to Top