Skip to content
Merged
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
22 changes: 19 additions & 3 deletions src/authentication/authenticationContext.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,10 @@ describe('AuthenticationContext', function() {
expect(await authenticationContext.isSignedIn()).toBe(false);
});

it('completes sign-out even when getIdToken throws due to expired token with no refresh source', async () => {
it('completes sign-out when user is not signed in (getIdToken returns null for anonymous user)', async () => {
(authenticationContext as any).accessTokenResponse = { isValid: () => false };

const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
await authenticationContext.signOut();
warnSpy.mockRestore();

expect(await authenticationContext.isSignedIn()).toBe(false);
});
Expand Down Expand Up @@ -304,6 +302,15 @@ describe('AuthenticationContext', function() {
expect(accessToken).toBe(fakeAccessToken);
});

it('returns null when access token is expired and no refresh source is available (anonymous user)', async () => {
(authenticationContext as any).accessTokenResponse = { isValid: () => false };
// No refresh token in localStorage, no refreshAccessToken callback

const token = await authenticationContext.getAccessToken();

expect(token).toBeNull();
});

it('refreshes the accessToken if it is no longer valid', async () => {
(authenticationContext as any).accessTokenResponse = { isValid: () => false };
localStorage.setItem('elfsquad_refresh_token', 'STORED_TOKEN');
Expand Down Expand Up @@ -615,6 +622,15 @@ describe('AuthenticationContext', function() {

describe('getIdToken', function() {

it('returns null when access token is expired and no refresh source is available (anonymous user)', async () => {
(authenticationContext as any).accessTokenResponse = { isValid: () => false };
// No refresh token in localStorage, no refreshAccessToken callback

const idToken = await authenticationContext.getIdToken();

expect(idToken).toBeNull();
});

it('returns the id token after refreshing when the access token is expired', async () => {
const refreshMock = jest.fn().mockResolvedValue({ accessToken: 'NEW_TOKEN', expiresIn: 3600, idToken: 'NEW_ID_TOKEN' });
authenticationContext = new AuthenticationContext({
Expand Down
7 changes: 5 additions & 2 deletions src/authentication/authenticationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export class AuthenticationContext {
}

if (!this.options.refreshAccessToken && !TokenStore.hasRefreshToken()) {
throw new Error('@elfsquad/authentication: Access token expired and no refresh source is available. Ensure offline_access is in the requested scope or provide a refreshAccessToken callback.');
return null;
}

if (this._refreshTokenPromise) {
Expand Down Expand Up @@ -291,7 +291,10 @@ export class AuthenticationContext {
// Delegate to getAccessToken() so the shared _refreshTokenPromise gate is used,
// preventing duplicate refresh calls when getAccessToken() and getIdToken() are
// awaited concurrently.
await this.getAccessToken();
const accessToken = await this.getAccessToken();
if (!accessToken) {
return null;
}
return this.accessTokenResponse.idToken;
}

Expand Down
Loading