Skip to content

Allow refresh: true in getAccessToken() #2055

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

Merged
merged 9 commits into from
Apr 25, 2025
63 changes: 63 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,69 @@ export async function middleware(request: NextRequest) {
}
```

### Forcing Access Token Refresh

In some scenarios, you might need to explicitly force the refresh of an access token, even if it hasn't expired yet. This can be useful if, for example, the user's permissions or scopes have changed and you need to ensure the application has the latest token reflecting these changes.

The `getAccessToken` method provides an option to force this refresh.

**App Router (Server Components, Route Handlers, Server Actions):**

When calling `getAccessToken` without request and response objects, you can pass an options object as the first argument. Set the `refresh` property to `true` to force a token refresh.

```typescript
// app/api/my-api/route.ts
import { getAccessToken } from '@auth0/nextjs-auth0';

export async function GET() {
try {
// Force a refresh of the access token
const { token, expiresAt } = await getAccessToken({ refresh: true });

// Use the refreshed token
// ...

return Response.json({ token, expiresAt });
} catch (error) {
console.error('Error getting access token:', error);
return Response.json({ error: 'Failed to get access token' }, { status: 500 });
}
}
```

**Pages Router (getServerSideProps, API Routes):**

When calling `getAccessToken` with request and response objects (from `getServerSideProps` context or an API route), the options object is passed as the third argument.

```typescript
// pages/api/my-pages-api.ts
import { getAccessToken, withApiAuthRequired } from '@auth0/nextjs-auth0';
import type { NextApiRequest, NextApiResponse } from 'next';

export default withApiAuthRequired(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
// Force a refresh of the access token
const { token, expiresAt } = await getAccessToken(req, res, {
refresh: true
});

// Use the refreshed token
// ...

res.status(200).json({ token, expiresAt });
} catch (error: any) {
console.error('Error getting access token:', error);
res.status(error.status || 500).json({ error: error.message });
}
});
```

By setting `{ refresh: true }`, you instruct the SDK to bypass the standard expiration check and request a new access token from the identity provider using the refresh token (if available and valid). The new token set (including the potentially updated access token, refresh token, and expiration time) will be saved back into the session automatically.
This will in turn, also update the `id_token` field of `tokenset` in the session.

## `<Auth0Provider />`

### Passing an initial user from the server
Expand Down
138 changes: 81 additions & 57 deletions src/server/auth-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
// When a route doesn't match, the handler returns a NextResponse.next() with status 200
expect(response.status).toBe(200);
});

it("should use the default value (true) for enableAccessTokenEndpoint when not explicitly provided", async () => {
const secret = await generateSecret(32);
const transactionStore = new TransactionStore({
Expand Down Expand Up @@ -4374,53 +4374,65 @@ ca/T0LLtgmbMmxSv/MmzIg==
const authClient = await createAuthClient({
signInReturnToPath: defaultReturnTo
});

// Mock the transactionStore.save method to verify the saved state
const originalSave = authClient['transactionStore'].save;
authClient['transactionStore'].save = vi.fn(async (cookies, state) => {
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
expect(state.returnTo).toBe(defaultReturnTo);
return originalSave.call(authClient['transactionStore'], cookies, state);
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});

await authClient.startInteractiveLogin();
expect(authClient['transactionStore'].save).toHaveBeenCalled();

expect(authClient["transactionStore"].save).toHaveBeenCalled();
});

it("should sanitize and use the provided returnTo parameter", async () => {
const authClient = await createAuthClient();
const returnTo = "/custom-return-path";

// Mock the transactionStore.save method to verify the saved state
const originalSave = authClient['transactionStore'].save;
authClient['transactionStore'].save = vi.fn(async (cookies, state) => {
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
// The full URL is saved, not just the path
expect(state.returnTo).toBe("https://example.com/custom-return-path");
return originalSave.call(authClient['transactionStore'], cookies, state);
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});

await authClient.startInteractiveLogin({ returnTo });
expect(authClient['transactionStore'].save).toHaveBeenCalled();

expect(authClient["transactionStore"].save).toHaveBeenCalled();
});

it("should reject unsafe returnTo URLs", async () => {
const authClient = await createAuthClient({
signInReturnToPath: "/safe-path"
});
const unsafeReturnTo = "https://malicious-site.com";

// Mock the transactionStore.save method to verify the saved state
const originalSave = authClient['transactionStore'].save;
authClient['transactionStore'].save = vi.fn(async (cookies, state) => {
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(async (cookies, state) => {
// Should use the default safe path instead of the malicious one
expect(state.returnTo).toBe("/safe-path");
return originalSave.call(authClient['transactionStore'], cookies, state);
return originalSave.call(
authClient["transactionStore"],
cookies,
state
);
});

await authClient.startInteractiveLogin({ returnTo: unsafeReturnTo });
expect(authClient['transactionStore'].save).toHaveBeenCalled();

expect(authClient["transactionStore"].save).toHaveBeenCalled();
});

it("should pass authorization parameters to the authorization URL", async () => {
Expand All @@ -4429,10 +4441,10 @@ ca/T0LLtgmbMmxSv/MmzIg==
audience: "https://api.example.com",
scope: "openid profile email custom_scope"
};

// Spy on the authorizationUrl method to verify the passed params
const originalAuthorizationUrl = authClient['authorizationUrl'];
authClient['authorizationUrl'] = vi.fn(async (params) => {
const originalAuthorizationUrl = authClient["authorizationUrl"];
authClient["authorizationUrl"] = vi.fn(async (params) => {
// Verify the audience is set correctly
expect(params.get("audience")).toBe(authorizationParameters.audience);
// Verify the scope is set correctly
Expand All @@ -4441,8 +4453,8 @@ ca/T0LLtgmbMmxSv/MmzIg==
});

await authClient.startInteractiveLogin({ authorizationParameters });
expect(authClient['authorizationUrl']).toHaveBeenCalled();

expect(authClient["authorizationUrl"]).toHaveBeenCalled();
});

it("should handle pushed authorization requests (PAR) correctly", async () => {
Expand All @@ -4452,11 +4464,11 @@ ca/T0LLtgmbMmxSv/MmzIg==
parRequestCalled = true;
}
});

const secret = await generateSecret(32);
const transactionStore = new TransactionStore({ secret });
const sessionStore = new StatelessSessionStore({ secret });

const authClient = new AuthClient({
transactionStore,
sessionStore,
Expand All @@ -4471,33 +4483,41 @@ ca/T0LLtgmbMmxSv/MmzIg==
},
fetch: mockFetch
});

await authClient.startInteractiveLogin();

// Verify that PAR was used
expect(parRequestCalled).toBe(true);
});

it("should save the transaction state with correct values", async () => {
const authClient = await createAuthClient();
const returnTo = "/custom-path";

// Instead of mocking the oauth functions, we'll just check the structure of the transaction state
const originalSave = authClient['transactionStore'].save;
authClient['transactionStore'].save = vi.fn(async (cookies, transactionState) => {
expect(transactionState).toEqual(expect.objectContaining({
nonce: expect.any(String),
codeVerifier: expect.any(String),
responseType: "code",
state: expect.any(String),
returnTo: "https://example.com/custom-path"
}));
return originalSave.call(authClient['transactionStore'], cookies, transactionState);
});
const originalSave = authClient["transactionStore"].save;
authClient["transactionStore"].save = vi.fn(
async (cookies, transactionState) => {
expect(transactionState).toEqual(
expect.objectContaining({
nonce: expect.any(String),
codeVerifier: expect.any(String),
responseType: "code",
state: expect.any(String),
returnTo: "https://example.com/custom-path"
})
);
return originalSave.call(
authClient["transactionStore"],
cookies,
transactionState
);
}
);

await authClient.startInteractiveLogin({ returnTo });
expect(authClient['transactionStore'].save).toHaveBeenCalled();

expect(authClient["transactionStore"].save).toHaveBeenCalled();
});

it("should merge configuration authorizationParameters with method arguments", async () => {
Expand All @@ -4509,13 +4529,13 @@ ca/T0LLtgmbMmxSv/MmzIg==
audience: configAudience
}
});

const methodScope = "openid profile email custom_scope";
const methodAudience = "https://custom-api.example.com";

// Spy on the authorizationUrl method to verify the passed params
const originalAuthorizationUrl = authClient['authorizationUrl'];
authClient['authorizationUrl'] = vi.fn(async (params) => {
const originalAuthorizationUrl = authClient["authorizationUrl"];
authClient["authorizationUrl"] = vi.fn(async (params) => {
// Method's authorization parameters should override config
expect(params.get("audience")).toBe(methodAudience);
expect(params.get("scope")).toBe(methodScope);
Expand All @@ -4528,14 +4548,14 @@ ca/T0LLtgmbMmxSv/MmzIg==
audience: methodAudience
}
});
expect(authClient['authorizationUrl']).toHaveBeenCalled();

expect(authClient["authorizationUrl"]).toHaveBeenCalled();
});

// Add tests for handleLogin method
it("should create correct options in handleLogin with returnTo parameter", async () => {
const authClient = await createAuthClient();

// Mock startInteractiveLogin to check what options are passed to it
const originalStartInteractiveLogin = authClient.startInteractiveLogin;
authClient.startInteractiveLogin = vi.fn(async (options) => {
Expand All @@ -4546,19 +4566,21 @@ ca/T0LLtgmbMmxSv/MmzIg==
return originalStartInteractiveLogin.call(authClient, options);
});

const reqUrl = new URL("https://example.com/auth/login?foo=bar&returnTo=custom-return");
const reqUrl = new URL(
"https://example.com/auth/login?foo=bar&returnTo=custom-return"
);
const req = new NextRequest(reqUrl, { method: "GET" });

await authClient.handleLogin(req);

expect(authClient.startInteractiveLogin).toHaveBeenCalled();
});

it("should handle PAR correctly in handleLogin by not forwarding params", async () => {
const authClient = await createAuthClient({
pushedAuthorizationRequests: true
});

// Mock startInteractiveLogin to check what options are passed to it
const originalStartInteractiveLogin = authClient.startInteractiveLogin;
authClient.startInteractiveLogin = vi.fn(async (options) => {
Expand All @@ -4569,11 +4591,13 @@ ca/T0LLtgmbMmxSv/MmzIg==
return originalStartInteractiveLogin.call(authClient, options);
});

const reqUrl = new URL("https://example.com/auth/login?foo=bar&returnTo=custom-return");
const reqUrl = new URL(
"https://example.com/auth/login?foo=bar&returnTo=custom-return"
);
const req = new NextRequest(reqUrl, { method: "GET" });

await authClient.handleLogin(req);

expect(authClient.startInteractiveLogin).toHaveBeenCalled();
});
});
Expand Down
Loading