Skip to content
Open
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
31 changes: 31 additions & 0 deletions .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,34 @@ jobs:

- name: Build example-fastify-api
run: npm run build -w example-fastify-api

example-fastify-web-call-api:
name: Web App Calling an API
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Setup Node.js with npm caching
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
package-manager-cache: false

- name: Update npm
run: npm install -g npm@11.10.0

- name: Install dependencies
run: npm install

# The example imports @auth0/auth0-fastify from its built dist, so the
# SDK must be built before the example can build or test.
- name: Build auth0-fastify
run: npm run build -w @auth0/auth0-fastify

- name: Build example-fastify-web-call-api
run: npm run build -w example-fastify-web-call-api

- name: Test example-fastify-web-call-api
run: npm run test:ci -w example-fastify-web-call-api
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Jump straight to the capability you need.
The following examples can be found in the examples directory:

- [Fastify Web App Example](./examples/example-fastify-web/README.md)
- [Fastify Web App Calling an API Example](./examples/example-fastify-web-call-api/README.md)
- [Fastify API Example](./examples/example-fastify-api/README.md)

Before running the examples, you need to install the dependencies for the monorepo and build all the packages.
Expand Down
5 changes: 4 additions & 1 deletion examples/example-fastify-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ fastify.register(() => {

const start = async () => {
try {
await fastify.listen({ port: 3000 });
// Defaults to 3000; set PORT to run on another port (e.g. 3001 when running
// alongside the example-fastify-web-call-api web app).
const port = Number(process.env.PORT ?? 3000);
await fastify.listen({ port });
} catch (err) {
fastify.log.error(err);
process.exit(1);
Expand Down
13 changes: 13 additions & 0 deletions examples/example-fastify-web-call-api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
AUTH0_DOMAIN=
AUTH0_CLIENT_ID=
AUTH0_CLIENT_SECRET=
AUTH0_SESSION_SECRET=
APP_BASE_URL=http://localhost:3000

# The identifier (audience) of the API this app calls on the user's behalf.
# This must match the audience the resource server validates.
AUTH0_AUDIENCE=

# Base URL of the resource server. Run examples/example-fastify-api alongside
# this app (on port 3001) to act as the resource server — see the README.
API_BASE_URL=http://localhost:3001
71 changes: 71 additions & 0 deletions examples/example-fastify-web-call-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Fastify Web App Calling an API Example

This example shows how to use [`@auth0/auth0-fastify`](../../packages/auth0-fastify)
to log a user in, request an access token for an API (`audience`), and call a
resource server on the user's behalf.

The resource server is the existing [Fastify API example](../example-fastify-api)
in this repo, which is protected by
[`@auth0/auth0-fastify-api`](../../packages/auth0-fastify-api). You run it as a
separate service alongside this web app.

## Install dependencies

From the repository root:

```bash
npm install
npm run build
```

## Configuration

This example needs an Auth0 **Regular Web Application** (for the web app) and an
Auth0 **API** (the `audience`). Configure both this example and the
`example-fastify-api` to use the same API.

Rename `.env.example` to `.env` here and fill in the values:

```env
AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN
AUTH0_CLIENT_ID=YOUR_CLIENT_ID
AUTH0_CLIENT_SECRET=YOUR_CLIENT_SECRET
AUTH0_SESSION_SECRET=A_LONG_RANDOM_SECRET
APP_BASE_URL=http://localhost:3000
AUTH0_AUDIENCE=YOUR_API_AUDIENCE
API_BASE_URL=http://localhost:3001
```

`AUTH0_AUDIENCE` must be the identifier of the API registered in your Auth0
tenant. The resource server validates tokens against this same audience.

The `AUTH0_SESSION_SECRET` is the key used to encrypt the session cookie. You
can generate a secret using `openssl`:

```shell
openssl rand -hex 64
```

> [!IMPORTANT]
> In the Auth0 Dashboard, add `http://localhost:3000/auth/callback` to **Allowed
> Callback URLs** and `http://localhost:3000` to **Allowed Logout URLs**.

## Run

Start the resource server (the API example) on port 3001 in one terminal:

```bash
# in examples/example-fastify-api (configure its .env with the same AUTH0_AUDIENCE)
PORT=3001 npm start
```

Start this web app on port 3000 in another terminal:

```bash
# in examples/example-fastify-web-call-api
npm start
```

Open http://localhost:3000, log in, then visit **Call API** (`/call-api`). The
web app requests an access token for `AUTH0_AUDIENCE` and calls
`GET /api/private` on the resource server with it, then renders the response.
28 changes: 28 additions & 0 deletions examples/example-fastify-web-call-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "example-fastify-web-call-api",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"start": "tsx src/index.ts --project tsconfig.json",
"build": "tsc --project tsconfig.json",
"test": "vitest run",
"test:ci": "vitest run"
},
"devDependencies": {
"@types/ejs": "^3.1.5",
"jose": "^5.9.6",
"msw": "^2.11.3",
"tsx": "^4.19.2",
"typescript": "~5.8.2",
"vitest": "^3.0.5"
},
"dependencies": {
"@auth0/auth0-fastify": "*",
"@fastify/static": "^9.1.1",
"@fastify/view": "^10.0.2",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"fastify": "^5.2.1"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
143 changes: 143 additions & 0 deletions examples/example-fastify-web-call-api/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
import type { FastifyInstance } from 'fastify';

// Env must be set BEFORE building the app (the SDK reads process.env when the
// plugin is registered). The values are placeholders; the network is fully
// mocked below.
const AUTH0_DOMAIN = 'tenant.auth0.local';
const CLIENT_ID = '<client_id>';
const AUDIENCE = 'https://api.example.com';

process.env.AUTH0_DOMAIN = AUTH0_DOMAIN;
process.env.AUTH0_CLIENT_ID = CLIENT_ID;
process.env.AUTH0_CLIENT_SECRET = '<client_secret>';
process.env.AUTH0_SESSION_SECRET = 'a-session-secret-of-at-least-32-characters-long';
process.env.APP_BASE_URL = 'http://localhost:3000';
process.env.AUTH0_AUDIENCE = AUDIENCE;
process.env.API_BASE_URL = 'http://api.local';

const KID = 'test-key-1';
let privateKey: CryptoKey;
let publicJwk: Record<string, unknown>;

const discovery = {
issuer: `https://${AUTH0_DOMAIN}/`,
authorization_endpoint: `https://${AUTH0_DOMAIN}/authorize`,
token_endpoint: `https://${AUTH0_DOMAIN}/oauth/token`,
end_session_endpoint: `https://${AUTH0_DOMAIN}/logout`,
jwks_uri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`,
};

const server = setupServer(
http.get(`https://${AUTH0_DOMAIN}/.well-known/openid-configuration`, () => HttpResponse.json(discovery)),
http.get(discovery.jwks_uri, () =>
HttpResponse.json({ keys: [{ ...publicJwk, kid: KID, alg: 'RS256', use: 'sig' }] })
),
http.post(discovery.token_endpoint, async () => {
const now = Math.floor(Date.now() / 1000);
const idToken = await new SignJWT({ name: 'Jane Doe', email: 'jane@example.com' })
.setProtectedHeader({ alg: 'RS256', kid: KID })
.setIssuer(discovery.issuer)
.setAudience(CLIENT_ID)
.setSubject('auth0|user_123')
.setIssuedAt(now)
.setExpirationTime(now + 3600)
.sign(privateKey);
const accessToken = await new SignJWT({ scope: 'openid profile' })
.setProtectedHeader({ alg: 'RS256', kid: KID })
.setIssuer(discovery.issuer)
.setAudience(AUDIENCE)
.setSubject('auth0|user_123')
.setIssuedAt(now)
.setExpirationTime(now + 3600)
.sign(privateKey);
return HttpResponse.json({
access_token: accessToken,
id_token: idToken,
token_type: 'Bearer',
expires_in: 3600,
});
}),
// The downstream resource server (examples/example-fastify-api), mocked. Its
// /api/private route returns plain text and requires a bearer token; here we
// echo the subject only when a Bearer token is present so the test can assert
// the token was forwarded.
http.get('http://api.local/api/private', ({ request: req }) => {
const auth = req.headers.get('authorization') ?? '';
if (!auth.startsWith('Bearer ')) {
return new HttpResponse('Unauthorized', { status: 401 });
}
return new HttpResponse('Hello, auth0|user_123');
})
);

// Join Set-Cookie header(s) into a Cookie request header (name=value pairs).
const cookieHeader = (h: string | string[] | undefined) =>
(Array.isArray(h) ? h : [h ?? '']).map((c) => c.split(';')[0]).join('; ');

let app: FastifyInstance;

beforeAll(async () => {
server.listen({ onUnhandledRequest: 'bypass' });
const kp = await generateKeyPair('RS256');
privateKey = kp.privateKey as CryptoKey;
publicJwk = await exportJWK(kp.publicKey);
const { buildApp } = await import('./index.js');
app = buildApp();
await app.ready();
});

afterEach(() => server.resetHandlers());
afterAll(async () => {
server.close();
await app.close();
});

// Drives login -> callback and returns the session Cookie header.
async function login(): Promise<string> {
const loginRes = await app.inject({ method: 'GET', url: '/auth/login' });
expect(loginRes.statusCode).toBe(302);
const txCookie = cookieHeader(loginRes.headers['set-cookie']);

const cbRes = await app.inject({
method: 'GET',
url: '/auth/callback?code=fake-code',
headers: { cookie: txCookie },
});
expect(cbRes.statusCode).toBe(302);
return cookieHeader(cbRes.headers['set-cookie']);
}

describe('example-fastify-web-call-api', () => {
test('login requests an access token for the configured audience', async () => {
const res = await app.inject({ method: 'GET', url: '/auth/login' });
const authorizeUrl = new URL(res.headers['location']?.toString() ?? '');

expect(res.statusCode).toBe(302);
expect(authorizeUrl.host).toBe(AUTH0_DOMAIN);
expect(authorizeUrl.searchParams.get('audience')).toBe(AUDIENCE);
});

test('after login, /call-api forwards the access token and renders the API response', async () => {
const sessionCookie = await login();

const res = await app.inject({
method: 'GET',
url: '/call-api',
headers: { cookie: sessionCookie },
});

expect(res.statusCode).toBe(200);
expect(res.body).toContain('Response from the API');
expect(res.body).toContain('Hello, auth0|user_123');
});

test('/call-api redirects to login when there is no session', async () => {
const res = await app.inject({ method: 'GET', url: '/call-api' });
expect(res.statusCode).toBe(302);
expect(res.headers['location']?.toString()).toContain('/auth/login');
});
});
Loading
Loading