Skip to main content

Overview

The EPR LAPS Backend API uses JWT (JSON Web Token) authentication with RS256 algorithm for securing API endpoints. The API integrates with Defra ID for identity management and uses OpenID Connect (OIDC) for token validation.
All API endpoints require authentication except the /health endpoint.

Authentication Flow

The API implements the following JWT authentication flow:

JWT Token Structure

The JWT token must contain the following claims:

Required Claims

ClaimTypeDescription
substringUser ID (subject)
rolesarrayArray of role strings in format <id>:<role>:<org>
relationshipsarrayUser’s organization relationships
currentRelationshipIdstringCurrent active relationship ID
issstringToken issuer (validated against config)

Token Example

{
  "sub": "user-123",
  "iss": "http://localhost:3200/cdp-defra-id-stub",
  "roles": ["rel-1:CEO:birmingham-city-council"],
  "relationships": [
    "rel-1:birmingham-city-council:birmingham-city-council",
    "rel-2:manchester-city-council:manchester-city-council"
  ],
  "currentRelationshipId": "rel-1",
  "iat": 1709481600,
  "exp": 1709485200
}

Configuration

The API requires the following authentication configuration:

Environment Variables

VariableDescriptionExample
DEFRA_ID_DISCOVERY_URLOIDC discovery endpointhttp://localhost:3200/cdp-defra-id-stub/.well-known/openid-configuration
DEFRA_ID_ISSUERExpected token issuerhttp://localhost:3200/cdp-defra-id-stub
The issuer claim in the JWT must exactly match the DEFRA_ID_ISSUER configuration value, or the token will be rejected.

Token Validation Process

The API performs the following validation steps (from src/plugins/auth.js:40-68):

1. JWKS Key Retrieval

The API fetches the JWKS (JSON Web Key Set) from the discovery document on startup:
// Cached discovery document with jwks_uri
const discoveryUrl = config.get('auth.discoveryUrl')
const discoveryRes = await Wreck.get(discoveryUrl, { json: true })
cachedDiscovery = discoveryRes.payload

2. Signature Verification

The JWT signature is verified using the public key from JWKS:
// Fetch JWKS keys and convert to PEM format
const { payload } = await Wreck.get(jwksUri, { json: true })
const keys = payload?.keys || []
const pem = jwkToPem(keys[0])
The token is verified using the RS256 algorithm with the issuer validation.

3. Token Claims Validation

The API validates the token claims (from src/plugins/auth.js:40-68):
export const jwtValidate = (decoded, request, _h) => {
  const { sub: userId, roles } = decoded
  const currentOrganisation = extractCurrentLocalAuthority(decoded)

  if (!roles) {
    return { isValid: false }
  }

  // Extract role from roles array
  let role = null
  if (Array.isArray(roles) && roles.length > 0) {
    const firstRoleParts = roles[0].split(':')
    role = firstRoleParts[1] || null
  }

  return {
    isValid: true,
    credentials: {
      userId,
      role,
      currentOrganisation,
      ...decoded
    }
  }
}

4. Organization Extraction

The current organization is extracted from the token relationships (from src/plugins/auth.js:70-86):
export const extractCurrentLocalAuthority = (token) => {
  let organisationName = ''
  if (Array.isArray(token.relationships) && token.currentRelationshipId) {
    const matched = token.relationships.find((rel) => {
      const parts = rel.split(':')
      return parts[0] === token.currentRelationshipId
    })

    if (matched) {
      const parts = matched.split(':')
      if (parts.length >= 3) {
        organisationName = parts[2]  // Organization name at index 2
      }
    }
  }
  return organisationName
}

Making Authenticated Requests

Request Format

Include the JWT token in the Authorization header using the Bearer scheme:
curl -X GET http://localhost:3001/documents/birmingham-city-council \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTEyMyIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzIwMC9jZHAtZGVmcmEtaWQtc3R1YiIsInJvbGVzIjpbInJlbC0xOkNFTzpiaXJtaW5naGFtLWNpdHktY291bmNpbCJdLCJyZWxhdGlvbnNoaXBzIjpbInJlbC0xOmJpcm1pbmdoYW0tY2l0eS1jb3VuY2lsOmJpcm1pbmdoYW0tY2l0eS1jb3VuY2lsIl0sImN1cnJlbnRSZWxhdGlvbnNoaXBJZCI6InJlbC0xIn0..."

Credentials Available in Handlers

After successful authentication, the following credentials are available in request handlers via request.auth.credentials:
PropertyTypeDescription
userIdstringUser ID from sub claim
rolestringUser role (e.g., “CEO”, “WO”)
currentOrganisationstringCurrent organization name
All JWT claimsvariousAll decoded JWT claims are also available

Example Handler Usage

const handler = (request, h) => {
  const { userId, role, currentOrganisation } = request.auth.credentials;
  
  request.logger.debug(`User ${userId} with role ${role} from ${currentOrganisation}`);
  
  // Use credentials for authorization logic
  if (role === 'CEO') {
    // Allow access
  }
  
  return h.response({ data: 'Protected data' });
};

Role-Based Permissions

The API supports role-based access control. Common roles include:
RoleCodeTypical Permissions
Chief Executive OfficerCEOFull access to all operations
Waste OfficerWOConfirm bank details
Role permissions are configured via environment variables. Check /permissions/config endpoint to see the current permission configuration.

Error Responses

401 Unauthorized

Returned when the JWT token is missing, invalid, or expired:
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Missing authentication"
}
Common causes:
  • Missing Authorization header
  • Malformed Bearer token
  • Expired token
  • Invalid signature
  • No JWKS keys found
  • Token issuer mismatch

403 Forbidden

Returned when the user is authenticated but lacks required permissions:
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
JWT tokens are verified on every request. Ensure your tokens are refreshed before expiry to avoid authentication failures.

Security Considerations

Token Verification

  • The API uses RS256 (RSA Signature with SHA-256) for asymmetric key verification
  • Public keys are fetched from the JWKS endpoint and cached
  • Signature verification happens on every request
  • Issuer claim is strictly validated against configuration

Best Practices

  1. Store tokens securely - Never expose JWT tokens in logs, URLs, or client-side code
  2. Use HTTPS - Always use HTTPS in production to prevent token interception
  3. Refresh tokens - Implement token refresh logic before expiry
  4. Validate permissions - Always check role-based permissions on the server side
  5. Monitor logs - Authorization headers are redacted in production logs for security

Log Redaction

In production, sensitive headers are automatically redacted (from src/config.js:74-76):
redact: {
  default: isProduction
    ? ['req.headers.authorization', 'req.headers.cookie', 'res.headers']
    : ['req', 'res', 'responseTime']
}

Testing Authentication

For local development with the Defra ID stub:
# Authenticate with the stub to get a token
curl -X POST http://localhost:3200/cdp-defra-id-stub/token \
  -H "Content-Type: application/json" \
  -d '{
    "username": "[email protected]",
    "password": "password"
  }'
The authentication stub is only available in local and development environments. Production uses the actual Defra ID service.

Build docs developers (and LLMs) love