@politico/lambda
v1.2.0
Published
A utility library to simplify setting up lambda functions
Downloads
69
Maintainers
Keywords
Readme
@politico/lambda
This library is meant to make it easier to spin up multi-use Lambda microservices integrated in an API Gateway. While it can be desireable to break Lambda functions down into their smallest possible forms, it's often more convenient to create Lambdas that do multiple things. For example, you might want to build a Lambda that reads from and writes to a database. Your project might even have several types of records and it's simplest to have one Lambda that handles CRUD operations for all of them. If you're used to something like Express, you may think about solving this problem by routing requests to different handler functions, but integrating Express in a Lambda environment can be a hassle. This library seeks to provide an Express-like routing experience while staying true to the simplicity of a Lambda environment.
Routing
In a simple case, setting up a Lambda router might look something like this:
import { createRouter, log, ok } from '@politico/lambda';
const router = createRouter({ log });
router.get('/hello/world', {
handler: () =>
ok({
response: {
message: 'Hello world!',
},
}),
});
export const handler = (event) => router.handleLambdaApiGateway(event);Here we're:
- creating a router that's set up to log requests to stdout,
- registering a route handler to respond to
GETrequests made to the/hello/worldendpoint, and - exporting a function called
handlerthat can operate as the entrypoint for a Lambda function.
With that, you should be able to create your Lambda, connect it as an
integration for an API Gateway, and ping your endpoint. Note that setting up
logging is optional. If you don't pass in a log object then logging will be
disabled, or you can pass in your own custom logger as long as it has
a logLevel function that accepts a log level and additional arguments to log.
Adding Auth
This library supports token-based authentication out of the box, relying on
an external service acting as the token authority. To configure your router
with authentication, you can provide auth options like this:
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: 'ssssshhhhh',
signinUrl: 'https://auth.com/signin',
roles: ['user', 'admin'],
},
});This then lets you configure routes with minimum roles set, like so:
router.get('/hello/world', {
handler: myHandler,
minimumRole: 'admin',
});Now only users with the admin role can access this endpoint!
By how does the router know which roles a user has, or even who the user is?
That's where the external token service comes in. The router will look for
a user's token either as a bearer token in an Authorization header or as
a newsroomAuthToken cookie. If it finds a token, it tries to parse it as
a JWT using the provided secret. If the token is expired, it tries to refresh
it by sending it to the token service at the provided signinUrl. If the token
can be neither verified nor refreshed, authentication fails.
The router assumes that the payload of the token will contain information about
the user as well as a set of permissions keyed by app name. The router will
attempt to look up app-specific permissions using the provided appName as the
key of the permissions object. A user may have multiple permissions (or
"roles") for the same app.
Once a user's roles are established for the specified app, the router can check
their authorization when attempting to access specific endpoints. This is where
the router's roles configuration comes into play. This array of roles
specifies two things: the set of roles supported by this app, and the relative
"level" of each role. A user will be able to access an endpoint with
a configured minimumRole if either (1) they have the exact specified role, or
(2) they have at least one role of a strictly greater level than the level of
the configured minimum role for that endpoint.
There are two ways you can specify the roles supported by a router. First, you can be fully explicit and specify each role's name and level:
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: 'ssssshhhhh',
signinUrl: 'https://auth.com/signin',
roles: [
{ name: 'user-type-1', level: 0 },
{ name: 'user-type-2', level: 0 },
{ name: 'admin', level: 1 },
],
},
});This can be helpful for more complex role schemes. Notice that in the above
example, both roles user-type-1 and user-type-2 have the level of 0 while
admin has a level of 1. This means that if an endpoint has a minimum role of
user-type-1, users with only the role user-type-2 won't be able to access
it, and vice versa. Users with the admin role will be able to access both
because the level of admin is greater than the levels of the user roles.
Often, an app's role scheme is more simply hierarchical and there are several increasing levels of access. As a convenience, you can specify roles as a simple array of names in this case:
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: 'ssssshhhhh',
signinUrl: 'https://auth.com/signin',
roles: ['user', 'admin'],
},
});Here, the level of each role is assumed to be its index in the array of roles,
so user has a level of 0 and admin has a role of 1.
Microsoft Entra SSO
To accept Microsoft Entra access tokens alongside the existing newsroom auth
tokens, add an entra block to auth. The router will detect Entra-issued
tokens, verify them against your tenant's JWKS, and then call your configured
signin service to load permissions from the same database you already use.
import { createRouter, log } from '@politico/lambda';
const router = createRouter({
logger: log,
auth: {
appName: 'my-app-name',
secret: process.env.NEWSROOM_AUTH_JWT_SECRET,
signinUrl: process.env.NEWSROOM_AUTH_SIGNIN_URL,
roles: ['user', 'admin'],
entra: {
serviceRole: 'service', // Assigns "service" role to client credential tokens automatically
secretId: ENTRA_SECRET_ID, // The ID for the AWS SM JSON
},
},
});The library reads the secret via Hermes (aws.secrets().getJSONSecret(...)),
caches it, and reuses your signin endpoint to resolve roles before enforcing
route-level authorization.
To validate a real Entra token locally, add the required values to .env and
run the opt-in live integration test with Hermes-compatible AWS access:
ENTRA_BEARER_TOKEN="..."
ENTRA_SECRET_ID="infra/auth/entra-example"
AUTH_APP_NAME="my-app-name"
EXPECTED_LOGIN="[email protected]"npm run test:auth:entraThe command verifies the token against Microsoft JWKS, loads Entra config from
Secrets Manager, looks up newsroomauth:user:<login> in DynamoDB, and prints
the resolved user identity, roles for AUTH_APP_NAME, and the full permissions
object. The test bundles before running so local dependency resolution matches
the package build. It does not print the bearer token or secret contents.
Optional env vars include ENTRA_PERMISSIONS_TABLE, EXPECTED_LOGIN,
EXPECTED_ROLE, and ALLOW_MISSING_APP_PERMISSIONS=true.
Integrating with API Gateway
A common and convenient way to deploy Lambda microservices is as integrations behind an API Gateway that handles routing to multiple services. When using path-based routing within a Lambda microservice, this can create some complications, because whatever path prefix the API Gateway uses to route the request to the appropriate Lambda is included in the request event that gets passed to the Lambda.
The router supports removing a path prefix for its internal routing purposes
automatically. To configure this, set the servicePathPrefix option:
import { createRouter, log, ok } from '@politico/lambda';
const router = createRouter({
log,
servicePathPrefix: '/service/my-service',
});Now, any request that comes in will get /service/my-service stripped off of
the start of the request path. Note that the router does not enforce that
requests match this prefix; it simply removes the prefix if it's there.
Other Utilities
This library is built to make creating Lambda microservices easier, so it includes a few additional helpers.
- loadEnvValue helps load values from the environment that will be type-safe as strings
- ok and error help format Lambda handler responses
- runDevServer runs an Express server that mimics an API Gateway integration for local development (note that this shouldn't be used in production)
