Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion packages/auth/src/api/authentication/exchange_token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ export interface ExchangeTokenRequest {
}

export interface ExchangeTokenResponse {
// The firebase access token (JWT signed by Firebase Auth).
accessToken: string;
expiresIn?: string;
// The time when the access token expires.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Is this in seconds?

Copy link
Contributor Author

@mansisampat mansisampat May 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, The expiresIn is in seconds.

expiresIn: number;
}

export async function exchangeToken(
Expand Down
10 changes: 9 additions & 1 deletion packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ import {
NextFn,
Unsubscribe,
PasswordValidationStatus,
TenantConfig
TenantConfig,
FirebaseToken
} from '../../model/public_types';
import {
createSubscribe,
Expand Down Expand Up @@ -100,6 +101,7 @@ export const enum DefaultConfig {
export class AuthImpl implements AuthInternal, _FirebaseService {
currentUser: User | null = null;
emulatorConfig: EmulatorConfig | null = null;
firebaseToken: FirebaseToken | null = null;
private operations = Promise.resolve();
private persistenceManager?: PersistenceUserManager;
private redirectPersistenceManager?: PersistenceUserManager;
Expand Down Expand Up @@ -455,6 +457,12 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
});
}

async _updateFirebaseToken(firebaseToken: FirebaseToken | null): Promise<void> {
if (firebaseToken) {
this.firebaseToken = firebaseToken;
}
}

async signOut(): Promise<void> {
if (_isFirebaseServerApp(this.app)) {
return Promise.reject(
Expand Down
10 changes: 9 additions & 1 deletion packages/auth/src/core/strategies/exchange_token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
testAuth,
TestAuth
} from '../../../test/helpers/mock_auth';
import * as sinon from 'sinon';
import * as mockFetch from '../../../test/helpers/mock_fetch';
import { HttpHeader, RegionalEndpoint } from '../../api';
import { exchangeToken } from './exhange_token';
Expand All @@ -35,19 +36,23 @@ use(chaiAsPromised);
describe('core/strategies/exchangeToken', () => {
let auth: TestAuth;
let regionalAuth: TestAuth;
let now: number;

beforeEach(async () => {
auth = await testAuth();
regionalAuth = await regionalTestAuth();
mockFetch.setUp();
now = Date.now();
sinon.stub(Date, 'now').returns(now);
});
afterEach(mockFetch.tearDown);
afterEach(() => sinon.restore());

it('should return a valid access token for Regional Auth', async () => {
const mock = mockRegionalEndpointWithParent(
RegionalEndpoint.EXCHANGE_TOKEN,
'projects/test-project-id/locations/us/tenants/tenant-1/idpConfigs/idp-config',
{ accessToken: 'outbound-token', expiresIn: '1000' }
{ accessToken: 'outbound-token', expiresIn: 10000 }
);

const accessToken = await exchangeToken(
Expand All @@ -65,6 +70,8 @@ describe('core/strategies/exchangeToken', () => {
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
'application/json'
);
expect(regionalAuth.firebaseToken?.token).to.equal('outbound-token');
expect(regionalAuth.firebaseToken?.expirationTime).to.equal(now + 10_000);
});

it('throws exception for default Auth', async () => {
Expand Down Expand Up @@ -106,5 +113,6 @@ describe('core/strategies/exchangeToken', () => {
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
'application/json'
);
expect(regionalAuth.firebaseToken).is.null;
});
});
7 changes: 6 additions & 1 deletion packages/auth/src/core/strategies/exhange_token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ export async function exchangeToken(
parent: buildParent(auth, idpConfigId),
token: customToken
});
// TODO(sammansi): Write token to the Auth object passed.
if (token) {
await authInternal._updateFirebaseToken({
token: token.accessToken,
expirationTime: Date.now() + token.expiresIn * 1000
});
}
return token.accessToken;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/model/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
AuthSettings,
Config,
EmulatorConfig,
FirebaseToken,
PasswordPolicy,
PasswordValidationStatus,
PopupRedirectResolver,
Expand Down Expand Up @@ -75,6 +76,7 @@ export interface AuthInternal extends Auth {
_initializationPromise: Promise<void> | null;
_persistenceManagerAvailable: Promise<void>;
_updateCurrentUser(user: UserInternal | null): Promise<void>;
_updateFirebaseToken(firebaseToken: FirebaseToken | null): Promise<void>;

_onStorageEvent(): void;

Expand Down
15 changes: 15 additions & 0 deletions packages/auth/src/model/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,14 @@ export interface Auth {
* {@link @firebase/app#FirebaseServerApp}.
*/
signOut(): Promise<void>;
/**
* The token response initialized via {@link exchangeToken} endpoint.
*
* @remarks
* This field is only supported for {@link Auth} instance that have defined
* {@link TenantConfig}.
*/
readonly firebaseToken: FirebaseToken | null;
}

/**
Expand Down Expand Up @@ -966,6 +974,13 @@ export interface ReactNativeAsyncStorage {
removeItem(key: string): Promise<void>;
}

export interface FirebaseToken {
// The firebase access token (JWT signed by Firebase Auth).
readonly token: string;
// The time when the access token expires.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this in seconds? Asking because I see you're * 1000 in the above change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's a timestamp (in milliseconds since epoch)? Could we clarify that in a comment to be more clear? Thanks!

readonly expirationTime: number;
}

/**
* A user account.
*
Expand Down
Loading