@bcgov/citz-imb-kc-express v0.0.1
BCGov SSO Keycloak Integration for Express
- Install package by following the steps at Installing the Package.
- Set up the package by following the steps at Basic Setup Guide.
- For use with @bcgov/citz-imb-kc-react.
Table of Contents
- General Information
- Installing the Package - Start Here!
- Basic Setup Guide - Setting up after installing.
- Environment Variables - Required variables for initialization.
- Directory Structure - How the repo is designed.
- Scripts - Scripts for running and working on the package.
- Module Exports - Functions and Types available from the module.
- TypeScript Types - Available TypeScript types.
- Initialization Options - Additional options.
- Authentication on an Endpoint - Require user to be signed in.
- Authorization on an Endpoint - Require user to have a role/permission.
- Authentication Flow - How it works.
- Applications using Keycloak Solution - See an example of how to use.
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.
Basic Setup Guide
- Add import
const { keycloak } = require('@bcgov/citz-imb-kc-express');
orimport { keycloak } from '@bcgov/citz-imb-kc-express';
to the top of the file that defines the express app. Addkeycloak(app);
below the definition of the express app, whereapp
is defined byexpress()
.
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);
- Add the required environment variables from the Environment Variables section below.
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
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.
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
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';
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;
};
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);
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);
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 thehasRoles()
function instead of checkingreq.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"]
}
Authentication Flow
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
1 year ago