Skip to content

feat: add configurable access token leeway parameter #2234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
63 changes: 63 additions & 0 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4865,6 +4865,69 @@ ca/T0LLtgmbMmxSv/MmzIg==
});
});

it("should refresh the access token if it expired — with leeway", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
secret
});
const sessionStore = new StatelessSessionStore({
secret
});
const authClient = new AuthClient({
transactionStore,
sessionStore,

domain: DEFAULT.domain,
clientId: DEFAULT.clientId,
clientSecret: DEFAULT.clientSecret,

secret,
appBaseUrl: DEFAULT.appBaseUrl,

routes: getDefaultRoutes(),

fetch: getMockAuthorizationServer({
tokenEndpointResponse: {
token_type: "Bearer",
access_token: "new-at",
expires_in: 3600 // expires in 1 hour
} as oauth.TokenEndpointResponse
}),

accessTokenExpiryLeeway: 30 * 60 // 30 minutes
});

// expires in 31 minutes, so it will not be refreshed because the leeway is 30 minutes
let tokenSet = {
accessToken: "old-at",
refreshToken: "old-rt",
expiresAt: Math.floor(Date.now() / 1000) + 31 * 60 // expires in 31 minutes
};

let [error, updatedTokenSet] = await authClient.getTokenSet(tokenSet);
expect(error).toBeNull();
expect(updatedTokenSet?.tokenSet).toEqual({
accessToken: "old-at",
refreshToken: "old-rt",
expiresAt: expect.any(Number)
});

// expires in 30 minutes, so it will be refreshed because the leeway is 30 minutes
tokenSet = {
accessToken: "old-at",
refreshToken: "old-rt",
expiresAt: Math.floor(Date.now() / 1000) + 30 * 60 // expires in 30 minutes
};

[error, updatedTokenSet] = await authClient.getTokenSet(tokenSet);
expect(error).toBeNull();
expect(updatedTokenSet?.tokenSet).toEqual({
accessToken: "new-at",
refreshToken: "old-rt",
expiresAt: expect.any(Number)
});
});

it("should return an error if an error occurred during the refresh token exchange", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
Expand Down
12 changes: 11 additions & 1 deletion src/server/auth-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,11 @@ export interface AuthClientOptions {
allowInsecureRequests?: boolean;
httpTimeout?: number;
enableTelemetry?: boolean;

enableAccessTokenEndpoint?: boolean;
noContentProfileResponseWhenUnauthenticated?: boolean;

accessTokenExpiryLeeway?: number;
}

function createRouteUrl(path: string, baseUrl: string) {
Expand Down Expand Up @@ -181,6 +184,8 @@ export class AuthClient {
private readonly enableAccessTokenEndpoint: boolean;
private readonly noContentProfileResponseWhenUnauthenticated: boolean;

private readonly accessTokenExpiryLeeway: number;

constructor(options: AuthClientOptions) {
// dependencies
this.fetch = options.fetch || fetch;
Expand Down Expand Up @@ -273,6 +278,8 @@ export class AuthClient {
this.enableAccessTokenEndpoint = options.enableAccessTokenEndpoint ?? true;
this.noContentProfileResponseWhenUnauthenticated =
options.noContentProfileResponseWhenUnauthenticated ?? false;

this.accessTokenExpiryLeeway = options.accessTokenExpiryLeeway ?? 0;
}

async handler(req: NextRequest): Promise<NextResponse> {
Expand Down Expand Up @@ -778,7 +785,10 @@ export class AuthClient {

if (tokenSet.refreshToken) {
// either the access token has expired or we are forcing a refresh
if (forceRefresh || tokenSet.expiresAt <= Date.now() / 1000) {
if (
forceRefresh ||
tokenSet.expiresAt <= Date.now() / 1000 + this.accessTokenExpiryLeeway
) {
const [discoveryError, authorizationServerMetadata] =
await this.discoverAuthorizationServerMetadata();

Expand Down
12 changes: 11 additions & 1 deletion src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ export interface Auth0ClientOptions {
* Defaults to `false`.
*/
noContentProfileResponseWhenUnauthenticated?: boolean;

/**
* The amount of time in seconds to refresh the access token before it expires.
*
* Defaults to `0` seconds.
*/
accessTokenExpiryLeeway?: number;
}

export type PagesRouterRequest = IncomingMessage | NextApiRequest;
Expand Down Expand Up @@ -311,9 +318,12 @@ export class Auth0Client {
allowInsecureRequests: options.allowInsecureRequests,
httpTimeout: options.httpTimeout,
enableTelemetry: options.enableTelemetry,

enableAccessTokenEndpoint: options.enableAccessTokenEndpoint,
noContentProfileResponseWhenUnauthenticated:
options.noContentProfileResponseWhenUnauthenticated
options.noContentProfileResponseWhenUnauthenticated,

accessTokenExpiryLeeway: options.accessTokenExpiryLeeway ?? 0
});
}

Expand Down