Skip to content
4 changes: 4 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,14 @@ toc:
path: /docs/reference/js/auth.recaptchaparameters.md
- title: RecaptchaVerifier
path: /docs/reference/js/auth.recaptchaverifier.md
- title: RefreshIdpTokenResult
path: /docs/reference/js/auth.refreshidptokenresult.md
- title: SAMLAuthProvider
path: /docs/reference/js/auth.samlauthprovider.md
- title: TenantConfig
path: /docs/reference/js/auth.tenantconfig.md
- title: TokenRefreshHandler
path: /docs/reference/js/auth.tokenrefreshhandler.md
- title: TotpMultiFactorAssertion
path: /docs/reference/js/auth.totpmultifactorassertion.md
- title: TotpMultiFactorGenerator
Expand Down
39 changes: 39 additions & 0 deletions docs-devsite/auth.auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface Auth
| [onAuthStateChanged(nextOrObserver, error, completed)](./auth.auth.md#authonauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
| [onIdTokenChanged(nextOrObserver, error, completed)](./auth.auth.md#authonidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
| [setPersistence(persistence)](./auth.auth.md#authsetpersistence) | Changes the type of persistence on the <code>Auth</code> instance. |
| [setTokenRefreshHandler(tokenRefreshHandler)](./auth.auth.md#authsettokenrefreshhandler) | Registers a handler for refreshing third-party identity provider (IDP) tokens.<!-- -->When the Firebase access token is expired, the SDK will automatically invoke the provided handler's <code>refreshIdpToken()</code> method to obtain a new IDP token. This new token will then be exchanged for a fresh Firebase token, streamlining the authentication process. |
| [signOut()](./auth.auth.md#authsignout) | Signs out the current user. This does not automatically revoke the user's ID token. |
| [updateCurrentUser(user)](./auth.auth.md#authupdatecurrentuser) | Asynchronously sets the provided user as [Auth.currentUser](./auth.auth.md#authcurrentuser) on the [Auth](./auth.auth.md#auth_interface) instance. |
| [useDeviceLanguage()](./auth.auth.md#authusedevicelanguage) | Sets the current language to the default device/browser preference. |
Expand Down Expand Up @@ -285,6 +286,44 @@ auth.setPersistence(browserSessionPersistence);

```

## Auth.setTokenRefreshHandler()

Registers a handler for refreshing third-party identity provider (IDP) tokens.

When the Firebase access token is expired, the SDK will automatically invoke the provided handler's `refreshIdpToken()` method to obtain a new IDP token. This new token will then be exchanged for a fresh Firebase token, streamlining the authentication process.

<b>Signature:</b>

```typescript
setTokenRefreshHandler(tokenRefreshHandler: TokenRefreshHandler): void;
```

#### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| tokenRefreshHandler | [TokenRefreshHandler](./auth.tokenrefreshhandler.md#tokenrefreshhandler_interface) | An object that implements the <code>TokenRefreshHandler</code> interface, providing the logic to refresh the IDP token. |

<b>Returns:</b>

void

### Example


```javascript
class TokenRefreshHandlerImpl {
refreshIdpToken() {
// Logic to fetch a new token from your custom IDP.
// Returns a Promise that resolves with a RefreshIdpTokenResult.
}
}

const tokenRefreshHandler = new TokenRefreshHandlerImpl();
auth.setTokenRefreshHandler(tokenRefreshHandler);

```

## Auth.signOut()

Signs out the current user. This does not automatically revoke the user's ID token.
Expand Down
2 changes: 2 additions & 0 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ Firebase Authentication
| [PopupRedirectResolver](./auth.popupredirectresolver.md#popupredirectresolver_interface) | A resolver used for handling DOM specific operations like [signInWithPopup()](./auth.md#signinwithpopup_770f816) or [signInWithRedirect()](./auth.md#signinwithredirect_770f816)<!-- -->. |
| [ReactNativeAsyncStorage](./auth.reactnativeasyncstorage.md#reactnativeasyncstorage_interface) | Interface for a supplied <code>AsyncStorage</code>. |
| [RecaptchaParameters](./auth.recaptchaparameters.md#recaptchaparameters_interface) | Interface representing reCAPTCHA parameters.<!-- -->See the [reCAPTCHA docs](https://developers.google.com/recaptcha/docs/display#render_param) for the list of accepted parameters. All parameters are accepted except for <code>sitekey</code>: Firebase Auth provisions a reCAPTCHA for each project and will configure the site key upon rendering.<!-- -->For an invisible reCAPTCHA, set the <code>size</code> key to <code>invisible</code>. |
| [RefreshIdpTokenResult](./auth.refreshidptokenresult.md#refreshidptokenresult_interface) | The result of a third-party IDP token refresh operation.<!-- -->This object contains the new IDP token and the Idp Config ID of the provider that issued it. |
| [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface) | The tenant config that can be used to initialize a Regional [Auth](./auth.auth.md#auth_interface) instance. |
| [TokenRefreshHandler](./auth.tokenrefreshhandler.md#tokenrefreshhandler_interface) | An interface for handling the refresh of Firebase tokens. |
| [TotpMultiFactorAssertion](./auth.totpmultifactorassertion.md#totpmultifactorassertion_interface) | The class for asserting ownership of a TOTP second factor. Provided by [TotpMultiFactorGenerator.assertionForEnrollment()](./auth.totpmultifactorgenerator.md#totpmultifactorgeneratorassertionforenrollment) and [TotpMultiFactorGenerator.assertionForSignIn()](./auth.totpmultifactorgenerator.md#totpmultifactorgeneratorassertionforsignin)<!-- -->. |
| [TotpMultiFactorInfo](./auth.totpmultifactorinfo.md#totpmultifactorinfo_interface) | The subclass of the [MultiFactorInfo](./auth.multifactorinfo.md#multifactorinfo_interface) interface for TOTP second factors. The <code>factorId</code> of this second factor is [FactorId](./auth.md#factorid)<!-- -->.TOTP. |
| [User](./auth.user.md#user_interface) | A user account. |
Expand Down
48 changes: 48 additions & 0 deletions docs-devsite/auth.refreshidptokenresult.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# RefreshIdpTokenResult interface
The result of a third-party IDP token refresh operation.

This object contains the new IDP token and the Idp Config ID of the provider that issued it.

<b>Signature:</b>

```typescript
export interface RefreshIdpTokenResult
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [idpConfigId](./auth.refreshidptokenresult.md#refreshidptokenresultidpconfigid) | string | The configuration ID of the third-party identity provider. |
| [idToken](./auth.refreshidptokenresult.md#refreshidptokenresultidtoken) | string | The new Id Token from the 3rd party Identity Provider. |

## RefreshIdpTokenResult.idpConfigId

The configuration ID of the third-party identity provider.

<b>Signature:</b>

```typescript
idpConfigId: string;
```

## RefreshIdpTokenResult.idToken

The new Id Token from the 3rd party Identity Provider.

<b>Signature:</b>

```typescript
idToken: string;
```
43 changes: 43 additions & 0 deletions docs-devsite/auth.tokenrefreshhandler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Project: /docs/reference/js/_project.yaml
Book: /docs/reference/_book.yaml
page_type: reference

{% comment %}
DO NOT EDIT THIS FILE!
This is generated by the JS SDK team, and any local changes will be
overwritten. Changes should be made in the source code at
https://github.com/firebase/firebase-js-sdk
{% endcomment %}

# TokenRefreshHandler interface
An interface for handling the refresh of Firebase tokens.

<b>Signature:</b>

```typescript
export interface TokenRefreshHandler
```

## Methods

| Method | Description |
| --- | --- |
| [refreshIdpToken()](./auth.tokenrefreshhandler.md#tokenrefreshhandlerrefreshidptoken) | Refreshes the third-party IDP token.<!-- -->This method should contain the logic to obtain a new, valid IDP token from your identity provider. |

## TokenRefreshHandler.refreshIdpToken()

Refreshes the third-party IDP token.

This method should contain the logic to obtain a new, valid IDP token from your identity provider.

<b>Signature:</b>

```typescript
refreshIdpToken(): Promise<RefreshIdpTokenResult>;
```
<b>Returns:</b>

Promise&lt;[RefreshIdpTokenResult](./auth.refreshidptokenresult.md#refreshidptokenresult_interface)<!-- -->&gt;

A promise that resolves with a `RefreshIdpTokenResult` object containing the new IDP token and its corresponding Idp Config ID.

134 changes: 133 additions & 1 deletion packages/auth/src/core/auth/auth_impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@ import { mockEndpointWithParams } from '../../../test/helpers/api/helper';
import { Endpoint, RecaptchaClientType, RecaptchaVersion } from '../../api';
import * as mockFetch from '../../../test/helpers/mock_fetch';
import { AuthErrorCode } from '../errors';
import * as exchangeTokenModule from '../strategies/exhange_token';
import {
FirebaseToken,
PasswordValidationStatus
PasswordValidationStatus,
TokenRefreshHandler
} from '../../model/public_types';
import { PasswordPolicyImpl } from './password_policy_impl';
import { PersistenceUserManager } from '../persistence/persistence_user_manager';
Expand Down Expand Up @@ -302,6 +304,136 @@ describe('core/auth/auth_impl', () => {
});
});

describe('#setTokenRefreshHandler', () => {
it('sets the tokenRefreshHandler on the auth object', () => {
const handler: TokenRefreshHandler = {
refreshIdpToken: async () => ({ idToken: 'a', idpConfigId: 'b' })
};
auth.setTokenRefreshHandler(handler);
expect((auth as any).tokenRefreshHandler).to.eq(handler);
});
});

describe('#getFirebaseAccessToken', () => {
let exchangeTokenStub: sinon.SinonStub;
let mockToken: FirebaseToken;
let expiredMockToken: FirebaseToken;
let tokenRefreshHandler: TokenRefreshHandler;
const tokenKey = `firebase:persistence-token:${FAKE_APP.options.apiKey!}:${
FAKE_APP.name
}`;

beforeEach(() => {
exchangeTokenStub = sinon
.stub(exchangeTokenModule, 'exchangeToken')
.resolves();

mockToken = {
token: 'test-token',
expirationTime: Date.now() + 300000 // 5 minutes from now
};
expiredMockToken = {
token: 'expired-test-token',
expirationTime: Date.now() - 1000 // 1 second ago
};
tokenRefreshHandler = {
refreshIdpToken: sinon.stub().resolves({
idToken: 'new-id-token',
idpConfigId: 'test-idp'
})
};
// Reset cached token and persistence before each test
(auth as any).firebaseToken = null;
persistenceStub._get.withArgs(tokenKey).resolves(null);
});

afterEach(() => {
sinon.restore();
});

it('should return the existing token if it is valid', async () => {
persistenceStub._get.withArgs(tokenKey).resolves(mockToken as any);
const token = await auth.getFirebaseAccessToken();
expect(token).to.eql(mockToken);
expect(exchangeTokenStub).not.to.have.been.called;
});

it('should return null if the token is expired and no token refresh handler is set', async () => {
persistenceStub._get.withArgs(tokenKey).resolves(expiredMockToken as any);
const token = await auth.getFirebaseAccessToken();
expect(token).to.be.null;
expect(exchangeTokenStub).not.to.have.been.called;
});

it('should refresh the token if it is expired and a token refresh handler is set', async () => {
persistenceStub._get.withArgs(tokenKey).resolves(expiredMockToken as any);
auth.setTokenRefreshHandler(tokenRefreshHandler);

exchangeTokenStub.callsFake(async () => {
// When exchangeToken is called, simulate that the new token is persisted.
persistenceStub._get.withArgs(tokenKey).resolves(mockToken as any);
});

const token = await auth.getFirebaseAccessToken();

expect(tokenRefreshHandler.refreshIdpToken).to.have.been.calledOnce;
expect(exchangeTokenStub).to.have.been.calledWith(
auth,
'test-idp',
'new-id-token'
);
expect(token).to.eql(mockToken);
});

it('should force refresh the token when forceRefresh is true', async () => {
persistenceStub._get.withArgs(tokenKey).resolves(mockToken as any);
auth.setTokenRefreshHandler(tokenRefreshHandler);

exchangeTokenStub.callsFake(async () => {
persistenceStub._get.withArgs(tokenKey).resolves(mockToken as any);
});

await auth.getFirebaseAccessToken(true);

expect(tokenRefreshHandler.refreshIdpToken).to.have.been.calledOnce;
expect(exchangeTokenStub).to.have.been.calledWith(
auth,
'test-idp',
'new-id-token'
);
});

it('should return null and log an error if token refresh fails', async () => {
const consoleErrorStub = sinon.stub(console, 'error');
persistenceStub._get.withArgs(tokenKey).resolves(expiredMockToken as any);
(tokenRefreshHandler.refreshIdpToken as sinon.SinonStub).rejects(
new Error('refresh failed')
);
auth.setTokenRefreshHandler(tokenRefreshHandler);
const token = await auth.getFirebaseAccessToken();
expect(token).to.be.null;
expect(consoleErrorStub).to.have.been.calledWith(
'Token refresh failed:',
sinon.match.instanceOf(Error)
);
});

it('should return null and log an error if the refreshed token is invalid', async () => {
const consoleErrorStub = sinon.stub(console, 'error');
persistenceStub._get.withArgs(tokenKey).resolves(expiredMockToken as any);
(tokenRefreshHandler.refreshIdpToken as sinon.SinonStub).resolves({
idToken: 'new-id-token'
}); // Missing idpConfigId
auth.setTokenRefreshHandler(tokenRefreshHandler);
const token = await auth.getFirebaseAccessToken();
expect(token).to.be.null;
expect(consoleErrorStub).to.have.been.calledWith(
'Token refresh failed:',
sinon.match.instanceOf(FirebaseError)
);
});
});

describe('#signOut', () => {
it('sets currentUser to null, calls remove', async () => {
await auth._updateCurrentUser(testUser(auth, 'test'));
Expand Down
Loading
Loading