Skip to content
Draft
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
184 changes: 184 additions & 0 deletions __tests__/Auth0Client/token-type-preservation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { verify } from '../../src/jwt';
import { MessageChannel } from 'worker_threads';
import * as utils from '../../src/utils';
import { expect } from '@jest/globals';

import { setupFn, fetchResponse, loginWithRedirectFn } from './helpers';
import {
TEST_ACCESS_TOKEN,
TEST_CODE_CHALLENGE,
TEST_ID_TOKEN,
TEST_REFRESH_TOKEN
} from '../constants';
Comment on lines +7 to +12

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note test

Unused import TEST_ACCESS_TOKEN.

Copilot Autofix

AI 5 months ago

To fix the issue, we should remove the unused TEST_ACCESS_TOKEN import from the file. This will clean up the code and eliminate the unnecessary import. No additional changes are required since the removal does not affect the functionality of the code.


Suggested changeset 1
__tests__/Auth0Client/token-type-preservation.test.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/__tests__/Auth0Client/token-type-preservation.test.ts b/__tests__/Auth0Client/token-type-preservation.test.ts
--- a/__tests__/Auth0Client/token-type-preservation.test.ts
+++ b/__tests__/Auth0Client/token-type-preservation.test.ts
@@ -7,3 +7,2 @@
 import {
-  TEST_ACCESS_TOKEN,
   TEST_CODE_CHALLENGE,
EOF
@@ -7,3 +7,2 @@
import {
TEST_ACCESS_TOKEN,
TEST_CODE_CHALLENGE,
Copilot is powered by AI and may make mistakes. Always verify output.

jest.mock('es-cookie');
jest.mock('../../src/jwt');
jest.mock('../../src/worker/token.worker');

const mockWindow = <any>global;
const mockFetch = <jest.Mock>mockWindow.fetch;
const mockVerify = <jest.Mock>verify;

jest
.spyOn(utils, 'bufferToBase64UrlEncoded')
.mockReturnValue(TEST_CODE_CHALLENGE);

const setup = setupFn(mockVerify);
const loginWithRedirect = loginWithRedirectFn(mockWindow, mockFetch);

describe('Auth0Client - Token Type Preservation', () => {
const oldWindowLocation = window.location;

beforeEach(() => {
delete (window as any).location;
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn()
}
}
) as Location;

mockWindow.open = jest.fn();
mockWindow.addEventListener = jest.fn();
mockWindow.removeEventListener = jest.fn();

mockWindow.crypto = {
subtle: { digest: () => 'foo' },
getRandomValues() {
return '123';
}
};
mockWindow.MessageChannel = MessageChannel;
mockWindow.Worker = {};
sessionStorage.clear();
localStorage.clear();
mockFetch.mockReset();
});

afterEach(() => {
jest.clearAllMocks();
window.location = oldWindowLocation;
});

describe('Token Type Preservation - HTTP Level', () => {
it('should preserve token_type when refreshing tokens via HTTP', async () => {
const auth0 = setup({
useRefreshTokens: true,
cacheLocation: 'localstorage'
});

// Perform login to set up authentication state with refresh token
await loginWithRedirect(auth0);
mockFetch.mockReset();

// Mock HTTP refresh token response that includes token_type
mockFetch.mockResolvedValueOnce(
fetchResponse(true, {
id_token: TEST_ID_TOKEN,
access_token: 'new_access_token_with_type',
refresh_token: 'new_refresh_token',
expires_in: 3600,
scope: 'openid profile email offline_access',
token_type: 'Bearer' // This should be preserved
})
);

const result = await auth0.getTokenSilently({
cacheMode: 'off',
detailedResponse: true
});

// Verify token_type flows through the HTTP refresh flow
expect(result).toMatchObject({
access_token: 'new_access_token_with_type',
token_type: 'Bearer'
});

// Verify actual HTTP call was made with refresh token grant
expect(mockFetch).toHaveBeenCalledTimes(1);
const requestBodyString = mockFetch.mock.calls[0][1].body;
expect(requestBodyString).toContain('grant_type=refresh_token');
});

it('should preserve token_type through cache storage after HTTP response', async () => {
const auth0 = setup({
useRefreshTokens: true,
cacheLocation: 'localstorage'
});

// Perform login to set up authentication state
await loginWithRedirect(auth0);
mockFetch.mockReset();

// Mock HTTP response with token_type
mockFetch.mockResolvedValueOnce(
fetchResponse(true, {
id_token: TEST_ID_TOKEN,
access_token: 'cached_token',
refresh_token: 'new_refresh_token',
expires_in: 3600,
scope: 'openid profile email offline_access',
token_type: 'Bearer'
})
);

// First call triggers HTTP request and caching
await auth0.getTokenSilently({ cacheMode: 'off' });

// Second call should use cache without HTTP - this tests cache preservation
const cachedResult = await auth0.getTokenSilently({
detailedResponse: true
});

// Verify token_type is preserved in cached response
expect(cachedResult).toMatchObject({
access_token: 'cached_token',
token_type: 'Bearer'
});

// Only one HTTP call should have been made
expect(mockFetch).toHaveBeenCalledTimes(1);
});

it('should handle HTTP response without token_type gracefully', async () => {
const auth0 = setup({
useRefreshTokens: true,
cacheLocation: 'localstorage'
});

// Perform login to set up authentication state
await loginWithRedirect(auth0);
mockFetch.mockReset();

// Mock HTTP response WITHOUT token_type (backward compatibility)
mockFetch.mockResolvedValueOnce(
fetchResponse(true, {
id_token: TEST_ID_TOKEN,
access_token: 'token_without_type',
refresh_token: TEST_REFRESH_TOKEN,
expires_in: 3600,
scope: 'openid profile email offline_access'
// Note: no token_type field
})
);

const result = await auth0.getTokenSilently({
cacheMode: 'off',
detailedResponse: true
});

// Should work without token_type (graceful degradation)
expect(result).toMatchObject({
access_token: 'token_without_type'
});
expect(result.token_type).toBeUndefined();

// Verify HTTP call was made
expect(mockFetch).toHaveBeenCalledTimes(1);
});
});
});
18 changes: 13 additions & 5 deletions src/Auth0Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,14 +716,20 @@ export class Auth0Client {
? await this._getTokenUsingRefreshToken(getTokenOptions)
: await this._getTokenFromIFrame(getTokenOptions);

const { id_token, access_token, oauthTokenScope, expires_in } =
authResult;
const {
id_token,
access_token,
oauthTokenScope,
expires_in,
token_type
} = authResult;

return {
id_token,
access_token,
...(oauthTokenScope ? { scope: oauthTokenScope } : null),
expires_in
expires_in,
...(token_type ? { token_type } : null)
};
} finally {
await lock.releaseLock(GET_TOKEN_SILENTLY_LOCK_KEY);
Expand Down Expand Up @@ -1072,14 +1078,16 @@ export class Auth0Client {
);

if (entry && entry.access_token) {
const { access_token, oauthTokenScope, expires_in } = entry as CacheEntry;
const { access_token, oauthTokenScope, expires_in, token_type } =
entry as CacheEntry;
const cache = await this._getIdTokenFromCache();
return (
cache && {
id_token: cache.id_token,
access_token,
...(oauthTokenScope ? { scope: oauthTokenScope } : null),
expires_in
expires_in,
...(token_type ? { token_type } : null)
}
);
}
Expand Down
1 change: 1 addition & 0 deletions src/cache/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export type CacheEntry = {
client_id: string;
refresh_token?: string;
oauthTokenScope?: string;
token_type?: string;
};

export type WrappedCacheEntry = {
Expand Down
7 changes: 4 additions & 3 deletions src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,10 @@ export interface Auth0ClientOptions extends BaseLoginOptions {

/**
* If provided, the SDK will load the token worker from this URL instead of the integrated `blob`. An example of when this is useful is if you have strict
* Content-Security-Policy (CSP) and wish to avoid needing to set `worker-src: blob:`. We recommend either serving the worker, which you can find in the module
* at `<module_path>/dist/auth0-spa-js.worker.production.js`, from the same host as your application or using the Auth0 CDN
* Content-Security-Policy (CSP) and wish to avoid needing to set `worker-src: blob:`. We recommend either serving the worker, which you can find in the module
* at `<module_path>/dist/auth0-spa-js.worker.production.js`, from the same host as your application or using the Auth0 CDN
* `https://cdn.auth0.com/js/auth0-spa-js/<version>/auth0-spa-js.worker.production.js`.
*
*
* **Note**: The worker is only used when `useRefreshTokens: true`, `cacheLocation: 'memory'`, and the `cache` is not custom.
*/
workerUrl?: string;
Expand Down Expand Up @@ -534,6 +534,7 @@ export type TokenEndpointResponse = {
refresh_token?: string;
expires_in: number;
scope?: string;
token_type?: string;
};

/**
Expand Down