@defra/lis-hubs-infra-access

中文文档
0.45.8发布于 2026/11/10OGL-UK-3.0ES Module构建来源已验证近 30 天下载 9923

Shared hub access and capability resolution for the livestock platform.

原始使用文档
文档目录16 节
  1. Installation
  2. Usage
  3. Hapi auth schemes
  4. How access decisions work
  5. Environment variables
  6. Prerequisites
  7. Setup
  8. Development
  9. Test
  10. Running in development
  11. Running tests
  12. Pipelines
  13. Branching policy
  14. Contributing to this project
  15. Licence
  16. About the licence

lis-hubs-infra-access

Part of the Livestock Information Service (LIS), which lets livestock keepers, owners, agents and delegates record and manage their animals.

@defra/lis-hubs-infra-access is a shared npm package that provides the hapi auth schemes, sessions and access decisions for LIS hubs and spokes. It handles sign-in with OpenID Connect (OIDC), the hub session, the hub and service tokens passed to spokes, and the translation of identity provider roles into LIS roles and permissions.

It deliberately does not hold provider-specific configuration or claim mapping. Each hub keeps its own: the front office uses Defra CI roles and the back office uses Microsoft Entra ID roles. It does not decide which modules a hub serves (see lis-hubs-infra-registry) or forward requests to spokes (see lis-hubs-infra-core).

Used by:

Installation

npm install @defra/lis-hubs-infra-access @defra/lis-hubs-infra-core @defra/lis-hubs-infra-registry @hapi/boom

@defra/lis-hubs-infra-core, @defra/lis-hubs-infra-registry and @hapi/boom are peer dependencies, so the consuming app must install them too. See package.json for the supported versions.

Usage

Hapi auth schemes

Hubs and spokes authenticate through two Hapi auth schemes, each registered as the server's default strategy. Identity is on request.auth.credentials.user (request.app carries no auth state); @defra/lis-infra-ui-services' nunjucks context and demandPermission read it from there. Routes opt out with auth: false (assets, /health) or accept signed-out users with auth: { mode: 'try' } or auth: { mode: 'optional' }. A signed-out try/optional request, in a hub or a spoke, carries no credentials, so caller isn't available on those requests.

Entry points:

ImportExports
@defra/lis-hubs-infra-access/authenticationcreateHubAuth, HUB_AUTH_STRATEGY, createSpokeAuth, SPOKE_AUTH_STRATEGY, createOidcClient, getHubJwt, issueHubJwt, verifyHubJwt
@defra/lis-hubs-infra-access/authorizationPERMISSIONS, hasPermission, demandPermission, resolveAuthorization, GLOBAL_CPH_SCOPE, AUTHORIZATION_VERSION
@defra/lis-hubs-infra-accessboth of the above as authentication and authorization namespaces

Prefer the subpath imports: they give plain named imports and simple mocks (vi.mock('@defra/lis-hubs-infra-access/authorization')).

import { createSpokeAuth } from '@defra/lis-hubs-infra-access/authentication'
import {
  hasPermission,
  PERMISSIONS
} from '@defra/lis-hubs-infra-access/authorization'

Spoke: createSpokeAuth

await server.register(
  createSpokeAuth({
    spokeId: 'cattle-home',
    hubOrigins,
    port,
    basePath,
    secret,
    audience,
    moduleAccess,
    authorize: async (user, request) => true // optional
  })
)

Registers the lis-spoke scheme as the default strategy. A request:

  1. must carry a valid service token in Authorization: Bearer <token>, else 401 { message: 'Service authentication required' } in every route mode, public spokes included;
  2. must carry a valid hub JWT in the x-lis-jwt-assertion header (JWT_ASSERTION_HEADER from @defra/lis-hubs-infra-core), unless the spoke is public. Without one, a required route redirects to <hubOrigin>/auth/login?returnUrl=…, and a try route continues unauthenticated;
  3. then gets credentials: { user, caller }, where caller is the hub's service name.

Authenticated requests are then checked against moduleAccess (non-public spokes) and the optional authorize predicate; failing either gives a 403. The factory throws for an unknown spokeId or an invalid moduleAccess.

The hub proxy sets the x-lis-jwt-assertion header on proxied requests; spokes never see the hub JWT as a cookie, and a JWT sent as a cookie is ignored. The header is only trusted because the service token check runs first, so createSpokeAuth takes no cookie options.

The service token is an AWS STS web identity token, attached by the hub's proxy in @defra/lis-hubs-infra-core. It must:

  • be an RS256 JWT signed by a key from CDP_JWT_JWKS_URI, issued by CDP_JWT_ISSUER, with an exp claim
  • have the audience lis-apps-<spokeId>
  • name the caller as lis-hubs-front-office or lis-hubs-back-office in the https://sts.amazonaws.com/ claim's principal_tags.ServiceName

CDP_JWT_ISSUER and CDP_JWT_JWKS_URI must be set when createSpokeAuth is called; the CDP platform provides them. The JWKS is fetched with the built-in fetch, which goes via the CDP proxy through the global dispatcher that setupProxy (@defra/lis-infra-ui-services) installs.

Hub: createHubAuth

await server.register(
  createHubAuth({
    hubId: 'front-office',
    getHubJwtConfig,
    resolveAuthSession,
    buildAuthorizationUrl,
    completeAuthorizationCodeGrant,
    buildLogoutUrl,
    getLogoutOrigins, // from createOidcClient, like buildLogoutUrl
    loginRoutes,
    notAuthorisedPath: '/auth/access-denied', // optional, defaults to '/auth/not-authorised'
    authorize: async (user, request) => true // optional
  })
)

Registers the login and /sso routes, POST /signout, GET /signout and GET /auth/logout (all auth: false) and the not-authorised route (auth: try), plus the lis-hub-session scheme as the default strategy, backed by the yar hub session. A signed-out user on a required route is redirected to the first login route with ?returnUrl=<path+search>; a try route continues unauthenticated. Credentials are { user, authorizedSpecies, caller }, where caller is the hub's own service name (lis-hubs-<hubId>), matching the caller a spoke sees. hubId is required and must be a registry hub id. The optional authorize predicate runs for authenticated requests only; false refuses the request (see below).

The hub JWT minted at sign-in is kept in the hub's yar session, not in a browser cookie, so the browser holds only the session cookie. getHubJwt(request) returns it (or null), and the hub's proxy injects it on proxied spoke requests in the x-lis-jwt-assertion header (JWT_ASSERTION_HEADER from @defra/lis-hubs-infra-core). Signing out resets the session, which drops the JWT with it.

The header Sign out (and the not-authorised page's) is a link to GET /signout, which never signs out: for a session with a CSRF token (only a signed-in one has one) it renders the auth/sign-out view from @defra/lis-infra-ui-services, asking the user to confirm with a Sign out button posting the token, or Cancel; anyone else is redirected to /. The old GET /auth/logout redirects to /signout.

POST /signout builds the identity provider's end-session URL, resets the session (even if building the URL fails, when the error propagates) and redirects there. The hub must register csrfPlugin from @defra/lis-infra-ui-services/csrf with { tokenSource: 'session' }, which requires the post to be same-origin and to carry the session's token in its _csrf field. The plugin is declared as a dependency, so a hub that has not registered it fails to start. A post that fails either check (an expired session has no token left, or the request is forged) signs nothing out and redirects to /, through the plugin's route failAction, rather than answering 403.

Browsers hold every redirect after a form post to the posting page's CSP form-action, which is 'self' on every hub page. So the confirmation page alone allows the origins the sign-out passes through: getLogoutOrigins(request) gives the end-session endpoint's origin, the issuer's (an identity provider on a custom domain redirects to its issuer host to finish) and the post-logout redirect URI's (from the configured hub origin, which may not be the one the user is on), from the same discovery metadata and provider as buildLogoutUrl. After the hub's Blankie plugin (blankie) has set the header, an onPreResponse hook appends them to the form-action directive of that response, leaving the rest of the policy as it is. Blankie's own route options cannot do it: they replace the whole policy and are fixed at route registration. If discovery fails, the confirmation page still renders, with form-action left as 'self', and the error is logged; the sign-out post then still resets the session.

A successful sign-in at /sso clears the session's CSRF token, so a new one is issued.

A hub-level refusal keeps authentication and authorisation apart: the user stays signed in to the hub but is not authorised to use it. resolveAuthSession refuses a login (e.g. a user not on an allow-list) by returning { denied: true }, and authorize refuses a signed-in user by returning false. authorize runs on every authenticated request and also in the login and /sso routes before a JWT is minted. Either way no JWT is stored in the session (any existing one is cleared), so no spoke is reachable, and the user is redirected to notAuthorisedPath. A refused login is stored as a session flagged refused, and every later authenticated request, including the login route, is sent to notAuthorisedPath without re-running the hub's checks, until the user signs out through the normal POST /signout flow. The identity provider is not involved in the refusal, so no post-logout redirect URI is needed for it. notAuthorisedPath must be a hub path starting with /, and the old accessDeniedPath option is rejected.

By default notAuthorisedPath is /auth/not-authorised, registered by this plugin with auth: try: it renders the error/not-authorised view (from @defra/lis-infra-ui-services) with a 403 and pageTitle, plus a signOutHref of /signout for a signed-in visitor or a signInHref of the first login route for anyone else. A hub that sets its own notAuthorisedPath registers that route itself and should do the same. Spoke-level 403s and record-level 404s are not affected.

Testing routes

Route tests need neither cookies nor service tokens; inject credentials:

await server.inject({
  url: '/',
  auth: {
    strategy: SPOKE_AUTH_STRATEGY,
    credentials: { user, caller }
  }
})

The package checks hub facts against @defra/lis-hubs-infra-registry. It does not hold hub policy.

When a hub signs a user in, it translates the provider roles into LIS roles. This package turns those roles into permissions.

How access decisions work

  • role definitions live in src/authorization/roles/roles.json and source mappings live in src/authorization/roles/role-mappings.json
  • permissions are derived from translated LIS roles, not trusted from identity providers
  • hub and hub-to-app JWTs carry LIS roles and an authorization model version, but do not carry expanded permissions or holdings
  • apps rehydrate permissions locally from the versioned role definitions before evaluating hasPermission or demandPermission
  • CPH-scoped role assignments remain scoped when permissions are rehydrated
  • lis-perm-front-office and lis-perm-back-office gate access to the corresponding hub
  • species permissions such as lis-perm-cattle-read apply across that species
  • app permissions such as lis-perm-cattle-register-admin apply to a specific species app
  • status and home resolve from the best permission found anywhere on the species
  • back-office permission management modules can be modeled with a type and unlocked by lis-perm-user-read, lis-perm-user-write, or lis-perm-user-admin

Environment variables

createSpokeAuth reads these. The CDP platform sets them. In lis-dev, the compose files set them.

VariableUsed for
CDP_JWT_ISSUERExpected issuer of service tokens
CDP_JWT_JWKS_URIWhere to fetch the keys that verify service token signatures

Prerequisites

  • Node.js 24 (see .nvmrc, use nvm use)
  • To check changes against a consuming app: a local clone of lis-dev with its one-time setup done

Setup

Development

nvm use
npm install

Test

There are no tools to install beyond npm install. Tests use Vitest.

Running in development

This is a library, so it has no server of its own. To try a change in a consuming app without publishing, link the local copy into the lis-dev stack:

cd lis-dev
.bin/sideload link hubs/infra-access # every installed copy now points at this repo
.bin/sideload up cattle-home        # optional: run a consumer locally with hot reload
.bin/sideload unlink hubs/infra-access # when done

Running tests

npm test                 # unit tests
npm run test:coverage    # with coverage
npm run lint             # ESLint
npm run format:check     # Prettier

Before releasing a change, also run the tests of each consuming repo against the linked package.

Pipelines

Build, versioning and publishing run in GitHub Actions (.github/workflows) using Cake scripts (*.cake):

  • Pull request to main: validate (format, lint, test), Sonar analysis, version check
  • Push to a feature/ or hotfix/ branch: prerelease version and tag
  • Merge to main: release tag, then publish to npm

Versions come from git tags (SemVer). After a merge, wait for the publish job to finish before bumping the package in consuming repos.

Branching policy

The repo uses mainline development. Branch from main and name the branch:

  • feature/LREG-<ticket>-<short-kebab-description>
  • hotfix/LREG-<ticket>-<short-kebab-description>

LREG must be uppercase, because the GitHub ruleset rejects other branch names. Rebase onto main rather than merging main in.

Contributing to this project

  • Follow the Defra software development standards.
  • npm install sets up a pre-commit hook (Husky) that runs the security audit, format check and lint.
  • Open a pull request against main using the PR template. CI must pass and the Sonar quality gate must be green before merge.
  • Feature branches bump the minor version and hotfix branches the patch version. While the package is on 0.x, a breaking change to the public API ships as a minor version, so call it out in the PR so consumers can update.

Licence

THIS INFORMATION IS LICENSED UNDER THE CONDITIONS OF THE OPEN GOVERNMENT LICENCE found at:

http://www.nationalarchives.gov.uk/doc/open-government-licence/version/3

The following attribution statement MUST be cited in your products and applications when using this information.

Contains public sector information licensed under the Open Government licence v3

About the licence

The Open Government Licence (OGL) was developed by the Controller of His Majesty's Stationery Office (HMSO) to enable information providers in the public sector to license the use and re-use of their information under a common open licence.

It is designed to encourage use and re-use of information freely and flexibly, with only a few conditions.

版本与发布记录

@defra/lis-hubs-infra-access 共发布 115 个版本,首次发布于 2026/08/10;过去 12 个月发布了 115 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.45.8latest2026/11/101 天前
0.45.72026/11/092 天前
0.45.62026/11/092 天前
0.45.52026/11/092 天前
0.45.42026/11/092 天前
0.45.4-lreg-000.alpha.1alpha2026/11/092 天前

关于 @defra/lis-hubs-infra-access 的常见问题

@defra/lis-hubs-infra-access 是什么?

Shared hub access and capability resolution for the livestock platform.

如何安装 @defra/lis-hubs-infra-access?

在项目目录运行 npm install @defra/lis-hubs-infra-access,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@hapi/boom(10.0.1)、@defra/lis-hubs-infra-core(>=0.31.0 <1.0.0)、@defra/lis-hubs-infra-registry(>=0.5.0 <1.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@defra/lis-hubs-infra-access 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/defra__lis-hubs-infra-access 是否存在。

@defra/lis-hubs-infra-access 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@defra/lis-hubs-infra-access 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@defra/lis-hubs-infra-access 可以商用吗?

@defra/lis-hubs-infra-access 使用 OGL-UK-3.0 许可。声明的许可为 OGL-UK-3.0,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@defra/lis-hubs-infra-access 的下载量有多少?

根据 npm 官方下载统计,@defra/lis-hubs-infra-access 在 2026/10/12 – 2026/11/10 这 30 天内被下载 9,923 次,日均约 331 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@defra/lis-hubs-infra-access 还在维护吗?

最新版本 0.45.8 发布于 2026/11/10(1 天前);至今共发布 115 个版本,其中过去 12 个月发布了 115 个;首次发布于 2026/08/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。