@oslokommune/auth-bff
v2.2.2
Published
A NodeJS Backend for frontend.
Readme
auth-bff
A NodeJS Backend for frontend.
Features:
- Serves a static web app
- Proxies API calls, with the user's access token
- Two "modes" of operation
- A vite plugin for use during development
- A standalone mode for use in production (e.g. inside a docker container)
- Supports generic OIDC auth code flow clients
- Has special support for
okdata-generated Idporten clients - Handles login/logout and sessions (using DynamoDb as a store)
- Includes simple React components for handling login-state
See https://github.com/oslokommune/auth-bff-example for an example React app using this package.
Getting started
Dev mode
- Install package
npm install @oslokommune/auth-bff- Add plugin to your vite config
import {defineConfig} from 'vite'
import bff from '@oslokommune/auth-bff/vite-plugin'
export default defineConfig({
plugins: [
bff()
],
...
})Create a config file
Run!
Standalone mode
- Install package globally
npm install -g @oslokommune/auth-bffCreate a config file
Run with
auth-bffRunning in Docker
When running in docker you should specify the version to use, and make sure it matches the one used in package.json.
Example dockerfile:
FROM node:24-alpine AS base
FROM base AS build
WORKDIR /home/app
COPY package*.json /home/app
RUN npm ci
COPY . ./
RUN npm run build
FROM base
WORKDIR /application
EXPOSE 8080
COPY --from=build /home/app/dist /application/dist
ENV NODE_ENV=production
RUN npm install -g @oslokommune/auth-bff@<version>
COPY bff.config.json /application/
CMD ["auth-bff"]To use different configuration for different environments, you can create separate config files for each and select it
at build time (using build args).
For example, with bff.config.dev.json and bff.config.prod.json:
ARG ENVIRONMENT=''
COPY bff.config.${ENVIRONMENT}.json /application/bff.config.json
CMD ["auth-bff"]Or select it at runtime, using an env var:
COPY bff.config*.json /application/
CMD exec auth-bff --configFile bff.config.${ENVIRONMENT}.jsonConfiguration
Configuration is defined in json-files that look like this:
{
"issuer": "https://example-issuer.com/",
"clientId": "example-client",
"clientSecret": "{ssm:/secret/from/parameter/store}",
"redirectUri": "http://localhost:3000/auth/callback",
"cookieSecure": false,
"cookieSameSite": false,
"postLogoutRedirectUri": "http://localhost:3000/",
"sessionSecret": "{env:SECRET_FROM_ENV}",
"sessionStoreType": "memory",
"proxyTargets": {
"/api": "http://localhost:8080/api"
}
}By default it looks for a file named bff.config.json, but this can be overridden:
Vite:
plugins: [
bff({configFile: ['bff.config.local.json', 'bff.config.json']}) //First existing file is used
]Standalone:
auth-bff --configFile /path/to/bff.config.jsonLoading values from environment or AWS Parameter Store
The config file supports two special forms for loading values from other sources. Primarily meant for loading secrets:
Environment values:
{
"sessionSecret": "{env:MY_SECRET}"
}AWS Parameter store:
{
"sessionSecret": "{ssm:/name/of/parameter}"
}This loads from the configured AWS environment. For this to work on your local machine the AWS_PROFILE environment
variable must be set, and you must be signed in to that profile
[!NOTE]
️ Seeconfig.tsfor a description of all config parameters
Mixing public and protected routes
If the application has some routes that should be reachable without an authenticated session (e.g. a
public catalog or a download endpoint polled by CI), list them under publicProxyTargets:
{
"proxyTargets": {
"/api": "http://localhost:8080/api"
},
"publicProxyTargets": {
"/api/public": "http://localhost:8080/api/public",
"/export.zip": "http://localhost:8080/export.zip"
}
}Public targets are proxied through anonymously — no session lookup, no Authorization header. The session cookie is stripped on the way through.
Public targets are registered before protected ones, so overlapping paths resolve to the public
mapping (e.g. publicProxyTargets["/api/public"] wins over proxyTargets["/api"]).
Using with ID-porten (via okdata):
auth-bff Has special support for keys generated by okdata.
- Start by creating a new client and key using okdata:
~> okdata pubs create-client
* Environment test
* Team <team>
* Client type ID-porten
* Integration name (identifying in which system or case this client will be used) <your name here>
* Redirect URIs (comma-separated) http://localhost:3000/auth/callback
* Post logout Redirect URIs (comma-separated) http://localhost:3000
* Frontchannel logout URI http://localhost:3000/auth/logout
* Client URI (back URI) http://localhost:3000
...
~> okdata pubs create-key
* Environment test
* Client <same client as above>
* Where should the key be stored? Send the key to your AWS Parameter Store
* AWS account number <accno>
* AWS region eu-west-1 (Ireland)
* Automatic key rotation will replace the key in SSM nightly on weekdays.
Enable automatic key rotation for this client? Yes
...
A new key has been created and the following parameters have been written to SSM:
- /okdata/maskinporten/11111111-2222-3333-4444-555555555555/key.json
...- Set the following options in the config file:
{
"issuer": "https://test.idporten.no/",
"clientId": "11111111-2222-3333-4444-555555555555",
"okDataIdPortenKeyName": "/okdata/maskinporten/11111111-2222-3333-4444-555555555555/key.json",
...
}Note that when using okDataIdPortenKeyName, that key is used for authentication, and clientSecret is not used.
Also, since the key is fetched from Parameter Store, you must set AWS_PROFILE and be signed in to that profile when
running locally.
- Done!
Using with Entra ID:
When using the package with Entra ID, you need to get credentials from Azure. You need to collect following credentials in order to be able to use this package:
- issuer
- client id
- client secret
With Entra ID you need to make sure to remove "okDataIdPortenKeyName"from the configuration file and replace it with client secret.
{
"issuer": "https://login.microsoftonline.com/{TENANT_ID}/v2.0",
"clientId": "1111111q-2bab-3333-c444-5555e556cb55",
"clientSecret": "7dW3Q~_sdfj3-4f5g-6789-h0i1-2j3k4l5m6n7",
...
}Configuring session storage
Currently only dynamoDb is supported for storing sessions in production. It requires some setup.
To work properly, the table must have ttl enabled, and an extra index for the idp-sid-property (used to delete
sessions during front-channel logout)
[!WARNING]
If the table does not exist, it will be automatically created with settings not appropriate for production.
Here is an example configuration in terraform. If you are using Golden Path,
you can simply copy this to a file in your application stack, and run terraform apply
resource "aws_dynamodb_table" "session_dynamodb_table" {
name = "${local.environment}-${local.main_container_name}-sessions"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
on_demand_throughput {
max_read_request_units = 20
max_write_request_units = 20
}
attribute {
name = "id"
type = "S"
}
attribute {
name = "idp-sid"
type = "S"
}
ttl {
enabled = true
attribute_name = "expires"
}
global_secondary_index {
hash_key = "idp-sid"
name = "idp-sid-index"
projection_type = "KEYS_ONLY"
on_demand_throughput {
max_read_request_units = 20
max_write_request_units = 20
}
}
}Then remember to update the config to use the new table:
{
"sessionStoreOptions": {
"table": "myteam-dev-myservice-sessions"
}
}The read/write limits above are just an example, and can be changed/removed
Required AWS permissions
When using SSM parameters or okDataIdPortenKeyName in the config, the service will need the ssm:GetParameter
permission for each parameter
If using dynamodb, the service will need the following permissions for the table
dynamodb:CreateTable
dynamodb:DescribeTable
dynamodb:PutItem
dynamodb:DeleteItem
dynamodb:GetItem
dynamodb:Scan
dynamodb:UpdateItemReact component
This package also includes a React component for handling authentication state. It will redirect to login if required and optionally automatically poll for changes to authentication state.
AuthContextProvider
import {AuthContextProvider} from "@oslokommune/auth-bff/react";
import {PktLoader} from "@oslokommune/punkt-react";
const fiveMinutes = 5 * 60 * 1000;
<AuthContextProvider authRequired={true} loaderComponent={<PktLoader/>} pollInterval={fiveMinues}>
<App/>
</AuthContextProvider>| Option | Description | |-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| | authRequired | Whether authentication is required. If true, will redirect to login before rendering child components (default: true) | | loaderComponent | React component to display while loading auth state. (default: null) | | baseUrl | Must be set to the same baseUrl as in the json config for login/logout to work correctly (default: '') | | pollInterval | Minimum interval in milliseconds between checks if session is still active. Will set authState to 'expired' if session is expired (default: disabled) |
useAuthContext
Hook to get current AuthState. Must be called in a component inside the AuthContextProvider.
import {useAuthContext} from "@oslokommune/auth-bff/react";
const {user, authState, login} = useAuthContext()
if (authState === 'authenticated') {
console.log(`Hello, ${user.pid}`)
} else {
login()
}
| Property | Description |
|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| user | The currently logged in user, or null if not logged in. User object contains the id_token claims returned from the usr endpoint. See config option userClaims. |
| login | Function that will redirect to the login endpoint when invoked |
| logout | Function that will redirect to the logout endpoint when invoked |
| authState | Current auth state. See table below for values | | |
AuthState
| Value | Description | |-----------------|------------------------------------------------------------------------------------------------------------------| | pending | Initial value before auth state has been determined | | authenticated | User is authenticated. | | unauthenticated | User is not authenticated | | expired | User was authenticated, but the session has expired. Can be used to display message to user or redirect to login | | | | error | Failed to determine auth state | | |
Content Security Policy
To configure the content security policy returned by the server, use the contentSecurityPolicy config option. This
configuration is passed almost as-is to helmet. Note that since our configuration is json
only, not all features are supported.
Nonce
To set a nonce, use the special form "{nonce}". It will be replaced by a
generated nonce for each request. For example:
{
"contentSecurityPolicy": {
"directives": {
...
"script-src": ["'self'", "{nonce}", ...]
}
}
}To use a nonce in your app, use __CSP_NONCE__ in your html. It will be replaced with a nonce for each request:
<script nonce="__CSP_NONCE__">
...
</script>Inject config
If your application requires configuration that needs to be determined at runtime, you can use injectConfig to inject
these values into a served html file:
{
"injectConfig": {
"enableSomeFeature": true,
"publicToken": "{env:MY_PUBLIC_TOKEN}"
}
}Then add something this to your index.html:
<script nonce="__CSP_NONCE__">
const injectedConfig = __INJECTED_CONFIG__
</script>When rendered, __INJECTED_CONFIG__ will be replaced with the values from injectConfig:
<script nonce="abcdef123456">
const injectedConfig = {
"enableSomeFeature": true,
"publicToken": "pub123abc"
}
</script>[!WARNING]
Do not put any secrets or other sensitive values in the injected config, they will be publicly visible.
