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
8 changes: 4 additions & 4 deletions examples/example-fastify-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fastify.register(fastifyAuth0, {


fastify.get('/', async (request, reply) => {
const user = await fastify.auth0Client!.getUser({ request, reply });
const user = await fastify.auth0Client!.getUser();

return reply.viewAsync('index.ejs', { isLoggedIn: !!user, user: user });
});
Expand All @@ -46,7 +46,7 @@ async function hasSessionPreHandler(
request: FastifyRequest,
reply: FastifyReply
) {
const session = await fastify.auth0Client!.getSession({ request, reply });
const session = await fastify.auth0Client!.getSession();

if (!session) {
reply.redirect(`/auth/login?returnTo=${request.url}`);
Expand All @@ -56,7 +56,7 @@ async function hasSessionPreHandler(
fastify.get(
'/public',
async (request, reply) => {
const user = await fastify.auth0Client!.getUser({ request, reply });
const user = await fastify.auth0Client!.getUser();

return reply.viewAsync('public.ejs', {
isLoggedIn: !!user,
Expand All @@ -71,7 +71,7 @@ fastify.get(
preHandler: hasSessionPreHandler,
},
async (request, reply) => {
const user = await fastify.auth0Client!.getUser({ request, reply });
const user = await fastify.auth0Client!.getUser();

return reply.viewAsync('private.ejs', {
isLoggedIn: !!user,
Expand Down
19 changes: 14 additions & 5 deletions packages/auth0-fastify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import type {
import fp from 'fastify-plugin';
import { CookieTransactionStore, ServerClient, StatelessStateStore, StatefulStateStore } from '@auth0/auth0-server-js';
import type { SessionConfiguration, SessionStore, StoreOptions } from './types.js';
import { createRouteUrl, toSafeRedirect } from './utils.js';
import { createRouteUrl, toFastifyInstance, toSafeRedirect } from './utils.js';
import { FastifyCookieHandler } from './store/fastify-cookie-handler.js';

export * from './types.js';
export { CookieTransactionStore } from '@auth0/auth0-server-js';
import { runWithContext } from './store/request-context.js';

declare module 'fastify' {
/**
Expand All @@ -26,7 +27,7 @@ declare module 'fastify' {
interface FastifyInstance<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
> {
/**
* The Auth0 Server Client instance attached to the Fastify instance.
Expand All @@ -41,7 +42,7 @@ declare module 'fastify' {
export interface Auth0FastifyOptions<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
> {
domain: string;
clientId: string;
Expand Down Expand Up @@ -86,7 +87,7 @@ export interface Auth0FastifyOptions<
export default fp(async function auth0Fastify<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
>(
fastify: FastifyInstance<RawServer, RawRequest, RawReply>,
options: Auth0FastifyOptions<RawServer, RawRequest, RawReply>
Expand Down Expand Up @@ -321,5 +322,13 @@ export default fp(async function auth0Fastify<
}
}

fastify.decorate('auth0Client', auth0Client);
// We rely on AsyncLocalStorage to store `FastifyRequest` and `FastifyReply` objects per request.
// This ensures we simplify the public API, as consumers no longer need to pass these instances to the methods.
fastify.addHook('onRequest', (request, reply, done) => {
// Run the rest of the request lifecycle (all subsequent hooks,
// handlers, and replies) inside the AsyncLocalStorage context.
runWithContext({ request, reply }, () => done());
});

fastify.decorate('auth0Client', toFastifyInstance(auth0Client));
});
67 changes: 67 additions & 0 deletions packages/auth0-fastify/src/store/request-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import { StoreOptions } from '../types.js';
import { RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerBase } from 'fastify';

/**
* Context containing Express request and response objects.
* Available within the AsyncLocalStorage scope established by the auth0 middleware.
*/
export type RequestContext = StoreOptions;

/**
* AsyncLocalStorage instance for storing request context.
* @internal
*/
const asyncLocalStorage = new AsyncLocalStorage<unknown>();

/**
* Runs a callback within an AsyncLocalStorage context containing the request and response.
* This establishes the context for the entire request lifecycle.
*
* @param request - Express request object
* @param response - Express response object
* @param callback - Function to execute within the context
* @returns The result of the callback
*
* @example
* ```typescript
* app.use((req, res, next) => {
* runWithContext(req, res, () => next());
* });
* ```
*/
export function runWithContext<T, TRequestContext>(context: TRequestContext, callback: () => T): T {
return asyncLocalStorage.run(context, callback);
}

/**
* Retrieves the current request context from AsyncLocalStorage.
*
* @returns The current RequestContext
* @throws {Error} If called outside of a request context
*
* @example
* ```typescript
* const { request, response } = getRequestContext();
* ```
*/
export function getRequestContext<
TRawServer extends RawServerBase,
TRawRequest extends RawRequestDefaultExpression<TRawServer>,
TRawReply extends RawReplyDefaultExpression<TRawServer>,
>(): StoreOptions<TRawServer, TRawRequest, TRawReply> {
const context = asyncLocalStorage.getStore();

if (!context) {
throw new Error(
'Request context not available. This error typically occurs when:\n' +
'1. Client methods are called outside of a request handler\n' +
'2. The auth0 SDK has not been initialized\n' +
'3. AsyncLocalStorage context was lost in an async operation\n\n' +
'Ensure you are calling client methods within a Fastify request handler ' +
'and the auth0 SDK is properly configured.'
);
}

return context as StoreOptions<TRawServer, TRawRequest, TRawReply>;
}
6 changes: 3 additions & 3 deletions packages/auth0-fastify/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { LogoutTokenClaims, StateData } from '@auth0/auth0-server-js';
/**
* Options for accessing the Fastify request and reply objects.
* These are used in store implementations to interact with cookies and sessions.
*
*
* FastifyInstance is a generic interface itself, whose generics represent the underlying server, request and reply types.
* By including these in the StoreOptions generics, we ensure that `StoreOptions` is aware of the underlying server type (e.g., HTTP/1.1, HTTP/2, etc.).
*
Expand All @@ -21,7 +21,7 @@ import { LogoutTokenClaims, StateData } from '@auth0/auth0-server-js';
export interface StoreOptions<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
> {
request: FastifyRequest<RouteGenericInterface, RawServer, RawRequest>;
reply: FastifyReply<RouteGenericInterface, RawServer, RawRequest, RawReply>;
Expand All @@ -30,7 +30,7 @@ export interface StoreOptions<
export interface SessionStore<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
> {
delete(identifier: string): Promise<void>;
set(identifier: string, stateData: StateData): Promise<void>;
Expand Down
105 changes: 105 additions & 0 deletions packages/auth0-fastify/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
import {
AccessTokenForConnectionOptions,
LoginBackchannelOptions,
LogoutOptions,
ServerClient,
StartInteractiveLoginOptions,
StartLinkUserOptions,
StartUnlinkUserOptions,
} from '@auth0/auth0-server-js';
import { RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerBase, RawServerDefault } from 'fastify';
import { StoreOptions } from './types.js';
import { getRequestContext } from './store/request-context.js';

/**
* Ensures the value has a trailing slash.
* If it does not, it will append one.
Expand Down Expand Up @@ -49,3 +62,95 @@ export function toSafeRedirect(dangerousRedirect: string, safeBaseUrl: string):

return undefined;
}

/**
* Converts a ServerClient to a FastifyInstance-bound client.
*
* This allows using the client methods without explicitly passing StoreOptions,
* as they will be automatically retrieved from the FastifyInstance's AsyncLocalStorage context (`requestContext`).
* @param serverClient The server client.
* @returns The FastifyInstance-bound client.
*/
export function toFastifyInstance<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
>(serverClient: ServerClient<StoreOptions<RawServer, RawRequest, RawReply>>) {
return {
startInteractiveLogin: (
options?: StartInteractiveLoginOptions,
storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>
) => {
return serverClient?.startInteractiveLogin(
options,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
completeInteractiveLogin: <TAppState>(url: URL, storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient?.completeInteractiveLogin<TAppState>(
url,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
getUser: (storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient?.getUser(storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>());
},
getSession: (storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient?.getSession(storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>());
},
getAccessToken: (storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient?.getAccessToken(storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>());
},
getAccessTokenForConnection: (
options: AccessTokenForConnectionOptions,
storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>
) => {
return serverClient?.getAccessTokenForConnection(
options,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
loginBackchannel: (
options: LoginBackchannelOptions,
storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>
) => {
return serverClient?.loginBackchannel(
options,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
logout: (options: LogoutOptions, storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient?.logout(options, storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>());
},
handleBackchannelLogout: (logoutToken: string, storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient?.handleBackchannelLogout(
logoutToken,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
startLinkUser: (options: StartLinkUserOptions, storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient.startLinkUser(options, storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>());
},
completeLinkUser: <TAppState>(url: URL, storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient.completeLinkUser<TAppState>(
url,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
startUnlinkUser: (
options: StartUnlinkUserOptions,
storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>
) => {
return serverClient.startUnlinkUser(
options,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
completeUnlinkUser: <TAppState>(url: URL, storeOptions?: StoreOptions<RawServer, RawRequest, RawReply>) => {
return serverClient.completeUnlinkUser<TAppState>(
url,
storeOptions ?? getRequestContext<RawServer, RawRequest, RawReply>()
);
},
} as ServerClient<StoreOptions<RawServer, RawRequest, RawReply>>;
}