Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

| Package | Version |
| ------- | ------- |
| `@labdigital/federated-token` | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token)](https://www.npmjs.com/package/@labdigital/federated-token) |
| `@labdigital/federated-token-apollo` | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-apollo)](https://www.npmjs.com/package/@labdigital/federated-token-apollo) |
| `@labdigital/federated-token-yoga` | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-yoga)](https://www.npmjs.com/package/@labdigital/federated-token-yoga) |
| `@labdigital/federated-token-express-adapter` | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-express-adapter)](https://www.npmjs.com/package/@labdigital/federated-token-express-adapter) |
| `@labdigital/federated-token-fastify-adapter` | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-fastify-adapter)](https://www.npmjs.com/package/@labdigital/federated-token-fastify-adapter) |
| `@labdigital/federated-token-react` | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-react)](https://www.npmjs.com/package/@labdigital/federated-token-react) |
| [`@labdigital/federated-token`](packages/core) | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token)](https://www.npmjs.com/package/@labdigital/federated-token) |
| [`@labdigital/federated-token-apollo`](packages/apollo) | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-apollo)](https://www.npmjs.com/package/@labdigital/federated-token-apollo) |
| [`@labdigital/federated-token-yoga`](packages/yoga) | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-yoga)](https://www.npmjs.com/package/@labdigital/federated-token-yoga) |
| [`@labdigital/federated-token-express-adapter`](packages/express-adapter) | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-express-adapter)](https://www.npmjs.com/package/@labdigital/federated-token-express-adapter) |
| [`@labdigital/federated-token-fastify-adapter`](packages/fastify-adapter) | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-fastify-adapter)](https://www.npmjs.com/package/@labdigital/federated-token-fastify-adapter) |
| [`@labdigital/federated-token-react`](packages/react) | [![npm](https://img.shields.io/npm/v/@labdigital/federated-token-react)](https://www.npmjs.com/package/@labdigital/federated-token-react) |

This repository contains multiple packages using JWT tokens for clients and
passing that information to all federated services (upstream and downstream).
Expand Down Expand Up @@ -46,7 +46,7 @@ const server = new ApolloServer({
new CookieTokenSource({
refreshTokenPath: "/auth/graphql",
secure: true,
sameSite: "Lax",
sameSite: "lax",
publicDomainFn: (request: express.Request) => config.COOKIE_DOMAIN,
cookieNames: {
refreshToken: "authRefreshToken",
Expand Down Expand Up @@ -84,3 +84,23 @@ an existing token.


Note that this expects the "cookie-parser" express middleware to be used.


# Development
Requires Node.js >= 22 and pnpm.

```sh
pnpm install
pnpm build # build all packages
pnpm test # run the tests
pnpm lint # biome check
pnpm tsc # type check
```

# Release
Releases are managed with [changesets](https://github.com/changesets/changesets).
Add a changeset with `pnpm changeset` in your pull request. Once merged, CI
opens a release PR; merging that publishes the packages to npm.

# License
MIT
156 changes: 156 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Federated Token - Core

[![npm](https://img.shields.io/npm/v/@labdigital/federated-token)](https://www.npmjs.com/package/@labdigital/federated-token)

The core package of [federated-token](../../README.md). It contains the token
model (`FederatedToken` / `PublicFederatedToken`), the `TokenSigner` and
`KeyManager` used to sign and encrypt the JWTs that are sent to clients, and the
framework-agnostic token source building blocks (`CompositeTokenSource`,
`BaseCookieTokenSource`, `BaseHeaderTokenSource`). It is used by the Apollo and
Yoga plugins and the Express and Fastify adapters. The package is ESM only.

## Installation

```sh
pnpm add @labdigital/federated-token graphql
# or
npm install @labdigital/federated-token graphql
```

## Requirements

- Node.js >= 22
- Peer dependency: `graphql` >= 16.6.0

## Usage

### Signing and encrypting tokens

`TokenSigner` signs the data JWT (HS256) and encrypts the access and refresh
JWTs (JWE, `dir` + `A256GCM`). It takes two `KeyManager` instances: one for
signing and one for encryption.

```ts
import { KeyManager, TokenSigner } from "@labdigital/federated-token";

export const createTokenSigner = () =>
new TokenSigner({
audience: "my-app",
issuer: "my-gateway",
signKeys: new KeyManager([
{ id: "1", key: Buffer.from(process.env.SIGN_KEY ?? "", "hex") },
]),
encryptKeys: new KeyManager([
{ id: "1", key: Buffer.from(process.env.ENCRYPT_KEY ?? "", "hex") },
]),
});
```

The signer is passed to `GatewayAuthPlugin` from
[`@labdigital/federated-token-apollo`](../apollo/README.md), which loads and
creates the tokens for each request.

### Working with the token

In a federated service the plugins store a `FederatedToken` on the context as
`federatedToken`. Resolvers use it to read and set tokens and values:

```ts
import type { FederatedTokenContext } from "@labdigital/federated-token";

const resolvers = {
Mutation: {
login: async (_: unknown, args: unknown, ctx: FederatedTokenContext) => {
const token = ctx.federatedToken;
if (!token) throw new Error("No federated token");

token.setAccessToken("commercetools", {
token: "<access-token>",
exp: Math.floor(Date.now() / 1000) + 3600,
sub: "customer-id",
});
token.setRefreshToken("commercetools", "<refresh-token>");
token.setValue("customerName", "Jane");
token.setIsAuthenticated();
return true;
},
logout: async (_: unknown, args: unknown, ctx: FederatedTokenContext) => {
ctx.federatedToken?.clear();
return true;
},
},
};
```

### Token sources

A token source reads tokens from a request and writes them to a response.
Framework specific implementations live in
[`@labdigital/federated-token-express-adapter`](../express-adapter/README.md)
and
[`@labdigital/federated-token-fastify-adapter`](../fastify-adapter/README.md).
`CompositeTokenSource` combines several sources: it reads from the first
source that returns a token and writes to all of them.

```ts
import { CompositeTokenSource } from "@labdigital/federated-token";
import {
CookieTokenSource,
HeaderTokenSource,
} from "@labdigital/federated-token-express-adapter";

const source = new CompositeTokenSource([
new CookieTokenSource({
secure: true,
sameSite: "lax",
refreshTokenPath: "/auth/graphql",
}),
new HeaderTokenSource(),
]);
```

To support another framework, extend `BaseCookieTokenSource` or
`BaseHeaderTokenSource` from `@labdigital/federated-token/tokensource` and
implement a `CookieAdapter` or `HeaderAdapter`.

## Configuration

`TokenSigner` options:

| Option | Type | Description |
| ------------- | ---------------------------------------- | ------------------------------------------------------------ |
| `signKeys` | `KeyManagerInterface` | Keys to sign and verify the data JWT (HS256). |
| `encryptKeys` | `KeyManagerInterface` | Keys to encrypt and decrypt the access and refresh JWTs. Must be 256-bit keys (`A256GCM`). |
| `audience` | `string \| string[]` | JWT audience. Required. |
| `issuer` | `string` | JWT issuer. Required. |
| `getSubject` | `(token: PublicFederatedToken) => string` | Optional. Sets the `sub` claim of the data JWT. |

`KeyManager` takes a list of `{ id, key }` objects, where `key` is a
`Uint8Array` or `KeyObject`. The first key is used to sign and encrypt new
tokens; all keys are used to verify and decrypt by their `kid`, so keys can be
rotated by putting a new key first. Implement `KeyManagerInterface` to load
keys from elsewhere.

Invalid or expired tokens throw `TokenInvalidError` or `TokenExpiredError`.

## Development

From the repository root:

```sh
pnpm install
pnpm build
pnpm test
pnpm lint
pnpm tsc
```

## Release

Releases are managed with [changesets](https://github.com/changesets/changesets).
Add a changeset with `pnpm changeset` in your pull request; packages are
published from CI once the release PR is merged.

## License

MIT
125 changes: 125 additions & 0 deletions packages/express-adapter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Federated Token - Express Adapter

[![npm](https://img.shields.io/npm/v/@labdigital/federated-token-express-adapter)](https://www.npmjs.com/package/@labdigital/federated-token-express-adapter)

Express implementations of the [federated-token](../../README.md) token
sources. `CookieTokenSource` reads and writes the tokens as cookies (for
browser clients) and `HeaderTokenSource` reads and writes them as HTTP headers
(for server-side clients). Pass them, optionally combined with
`CompositeTokenSource`, to `GatewayAuthPlugin` from
[`@labdigital/federated-token-apollo`](../apollo/README.md). The package is ESM
only.

## Installation

```sh
pnpm add @labdigital/federated-token-express-adapter express cookie-parser
# or
npm install @labdigital/federated-token-express-adapter express cookie-parser
```

## Requirements

- Node.js >= 22
- Peer dependency: `express` >= 4
- `CookieTokenSource` reads `request.cookies`, which Express does not populate
by itself. Register [`cookie-parser`](https://www.npmjs.com/package/cookie-parser)
(or another middleware that sets `request.cookies`), otherwise reading the
cookies throws. It is not a peer dependency; `HeaderTokenSource` works
without it.

## Usage

```ts
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@as-integrations/express5";
import { CompositeTokenSource } from "@labdigital/federated-token";
import { GatewayAuthPlugin } from "@labdigital/federated-token-apollo";
import {
CookieTokenSource,
HeaderTokenSource,
} from "@labdigital/federated-token-express-adapter";
import cookieParser from "cookie-parser";
import express from "express";

const server = new ApolloServer({
gateway,
plugins: [
new GatewayAuthPlugin({
signer: createTokenSigner(),
source: new CompositeTokenSource([
new CookieTokenSource({
secure: true,
sameSite: "lax",
refreshTokenPath: "/auth/graphql",
publicDomainFn: (request) => "example.com",
}),
new HeaderTokenSource(),
]),
}),
],
});
await server.start();

const app = express();
app.use(cookieParser());
app.use(
"/graphql",
express.json(),
expressMiddleware(server, {
context: async ({ req, res }) => ({ req, res }),
}),
);
```

`gateway` is your `ApolloGateway` instance and `createTokenSigner()` returns a
`TokenSigner`, see the
[core package](../core/README.md). The context must contain `req` and `res`
so the plugin can read and write the tokens.

## Configuration

`CookieTokenSource` options:

| Option | Type | Description |
| ------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| `secure` | `boolean` | Sets the `Secure` cookie flag. Required. |
| `sameSite` | `"strict" \| "lax" \| "none" \| boolean` | Sets the `SameSite` cookie attribute. Required. |
| `refreshTokenPath` | `string \| (request) => string \| undefined` | Path of the HTTP-only refresh token cookie. Required. |
| `publicDomainFn` | `(request) => string \| undefined` | Domain for the cookies. |
| `privateDomainFn` | `(request) => string \| undefined` | Exposed via the adapter, not used by the cookie source itself. |
| `cookiePathFn` | `(request) => string \| undefined` | Path of the non-refresh cookies. Defaults to `/`. |
| `cookieNames` | `Partial<CookieNames>` | Override the cookie names (see below). |
| `guestToken` | `{ expiresIn: number \| "session" }` | Expiry in seconds of the guest token and data cookies. Defaults to session. |
| `userToken` | `{ expiresIn: number \| "session" }` | Expiry in seconds of the user token and data cookies. Defaults to session. |
| `refreshToken` | `{ expiresIn: number \| "session" }` | Expiry in seconds of the refresh token cookies. Defaults to one year. |

Default cookie names: `userToken`, `guestToken` (HTTP-only access tokens),
`userData`, `guestData` (readable data tokens), `refreshToken` (HTTP-only) and
`userRefreshTokenExists` / `guestRefreshTokenExists`.

`HeaderTokenSource` takes an optional `{ headerNames }` to override the
default `x-access-token`, `x-data-token` and `x-refresh-token` headers. A
`Bearer ` prefix on the access token header is stripped.

## Development

From the repository root:

```sh
pnpm install
pnpm build
pnpm test
pnpm lint
pnpm tsc
```

## Release

Releases are managed with [changesets](https://github.com/changesets/changesets).
Add a changeset with `pnpm changeset` in your pull request; packages are
published from CI once the release PR is merged.

## License

MIT
Loading
Loading