Skip to content

Implement getToken method for Regional Auth Interop #9061

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

Open
wants to merge 12 commits into
base: gcip-byociam-web
Choose a base branch
from
2 changes: 2 additions & 0 deletions docs-devsite/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ toc:
path: /docs/reference/js/auth.emulatorconfig.md
- title: FacebookAuthProvider
path: /docs/reference/js/auth.facebookauthprovider.md
- title: FirebaseToken
path: /docs/reference/js/auth.firebasetoken.md
- title: GithubAuthProvider
path: /docs/reference/js/auth.githubauthprovider.md
- title: GoogleAuthProvider
Expand Down
13 changes: 13 additions & 0 deletions docs-devsite/auth.auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Auth
| [config](./auth.auth.md#authconfig) | [Config](./auth.config.md#config_interface) | The [Config](./auth.config.md#config_interface) used to initialize this instance. |
| [currentUser](./auth.auth.md#authcurrentuser) | [User](./auth.user.md#user_interface) \| null | The currently signed-in user (or null). |
| [emulatorConfig](./auth.auth.md#authemulatorconfig) | [EmulatorConfig](./auth.emulatorconfig.md#emulatorconfig_interface) \| null | The current emulator configuration (or null). |
| [firebaseToken](./auth.auth.md#authfirebasetoken) | [FirebaseToken](./auth.firebasetoken.md#firebasetoken_interface) \| null | The token response initialized via [exchangeToken()](./auth.md#exchangetoken_b6b1871) endpoint. |
| [languageCode](./auth.auth.md#authlanguagecode) | string \| null | The [Auth](./auth.auth.md#auth_interface) instance's language code. |
| [name](./auth.auth.md#authname) | string | The name of the app associated with the <code>Auth</code> service instance. |
| [settings](./auth.auth.md#authsettings) | [AuthSettings](./auth.authsettings.md#authsettings_interface) | The [Auth](./auth.auth.md#auth_interface) instance's settings. |
Expand Down Expand Up @@ -87,6 +88,18 @@ The current emulator configuration (or null).
readonly emulatorConfig: EmulatorConfig | null;
```

## Auth.firebaseToken

The token response initialized via [exchangeToken()](./auth.md#exchangetoken_b6b1871) endpoint.

This field is only supported for [Auth](./auth.auth.md#auth_interface) instance that have defined [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface)<!-- -->.

<b>Signature:</b>

```typescript
readonly firebaseToken: FirebaseToken | null;
```

## Auth.languageCode

The [Auth](./auth.auth.md#auth_interface) instance's language code.
Expand Down
40 changes: 40 additions & 0 deletions docs-devsite/auth.firebasetoken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 %}

# FirebaseToken interface
<b>Signature:</b>

```typescript
export interface FirebaseToken
```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [expirationTime](./auth.firebasetoken.md#firebasetokenexpirationtime) | number | |
| [token](./auth.firebasetoken.md#firebasetokentoken) | string | |

## FirebaseToken.expirationTime

<b>Signature:</b>

```typescript
readonly expirationTime: number;
```

## FirebaseToken.token

<b>Signature:</b>

```typescript
readonly token: string;
```
1 change: 1 addition & 0 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Firebase Authentication
| [ConfirmationResult](./auth.confirmationresult.md#confirmationresult_interface) | A result from a phone number sign-in, link, or reauthenticate call. |
| [Dependencies](./auth.dependencies.md#dependencies_interface) | The dependencies that can be used to initialize an [Auth](./auth.auth.md#auth_interface) instance. |
| [EmulatorConfig](./auth.emulatorconfig.md#emulatorconfig_interface) | Configuration of Firebase Authentication Emulator. |
| [FirebaseToken](./auth.firebasetoken.md#firebasetoken_interface) | |
| [IdTokenResult](./auth.idtokenresult.md#idtokenresult_interface) | Interface representing ID token result obtained from [User.getIdTokenResult()](./auth.user.md#usergetidtokenresult)<!-- -->. |
| [MultiFactorAssertion](./auth.multifactorassertion.md#multifactorassertion_interface) | The base class for asserting ownership of a second factor. |
| [MultiFactorError](./auth.multifactorerror.md#multifactorerror_interface) | The error thrown when the user needs to provide a second factor to sign in successfully. |
Expand Down
1 change: 1 addition & 0 deletions packages/auth-interop-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface FirebaseAuthTokenData {

export interface FirebaseAuthInternal {
getToken(refreshToken?: boolean): Promise<FirebaseAuthTokenData | null>;
getFirebaseToken(): Promise<FirebaseAuthTokenData | null>;
getUid(): string | null;
addAuthTokenListener(fn: (token: string | null) => void): void;
removeAuthTokenListener(fn: (token: string | null) => void): void;
Expand Down
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.
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> {
this.firebaseToken = firebaseToken;
}

async signOut(): Promise<void> {
if (_isFirebaseServerApp(this.app)) {
return Promise.reject(
Expand Down
63 changes: 62 additions & 1 deletion packages/auth/src/core/auth/firebase_internal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import { expect, use } from 'chai';
import * as sinon from 'sinon';
import chaiAsPromised from 'chai-as-promised';

import { testAuth, testUser } from '../../../test/helpers/mock_auth';
import {
regionalTestAuth,
testAuth,
testUser
} from '../../../test/helpers/mock_auth';
import { AuthInternal } from '../../model/auth';
import { UserInternal } from '../../model/user';
import { AuthInterop } from './firebase_internal';
Expand All @@ -37,6 +41,9 @@ describe('core/auth/firebase_internal', () => {

afterEach(() => {
sinon.restore();
delete (auth as unknown as Record<string, unknown>)[
'_initializationPromise'
];
});

context('getUid', () => {
Expand Down Expand Up @@ -215,3 +222,57 @@ describe('core/auth/firebase_internal', () => {
});
});
});

describe('core/auth/firebase_internal - Regional Firebase Auth', () => {
let regionalAuth: AuthInternal;
let regionalAuthInternal: AuthInterop;
let now: number;
beforeEach(async () => {
regionalAuth = await regionalTestAuth();
regionalAuthInternal = new AuthInterop(regionalAuth);
now = Date.now();
sinon.stub(Date, 'now').returns(now);
});

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

context('getFirebaseToken', () => {
it('returns null if firebase token is undefined', async () => {
expect(await regionalAuthInternal.getFirebaseToken()).to.be.null;
});

it('returns the id token correctly', async () => {
await regionalAuth._updateFirebaseToken({
token: 'access-token',
expirationTime: now + 300_000
});
expect(await regionalAuthInternal.getFirebaseToken()).to.eql({
accessToken: 'access-token'
});
});

it('logs out the the id token expires in next 30 seconds', async () => {
expect(await regionalAuthInternal.getFirebaseToken()).to.be.null;
});

it('logs out if token has expired', async () => {
await regionalAuth._updateFirebaseToken({
token: 'access-token',
expirationTime: now - 5_000
});
expect(await regionalAuthInternal.getFirebaseToken()).to.null;
expect(regionalAuth.firebaseToken).to.null;
});

it('logs out if token is expiring in next 5 seconds', async () => {
await regionalAuth._updateFirebaseToken({
token: 'access-token',
expirationTime: now + 5_000
});
expect(await regionalAuthInternal.getFirebaseToken()).to.null;
expect(regionalAuth.firebaseToken).to.null;
});
});
});
26 changes: 26 additions & 0 deletions packages/auth/src/core/auth/firebase_internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface TokenListener {
}

export class AuthInterop implements FirebaseAuthInternal {
private readonly TOKEN_EXPIRATION_BUFFER = 30_000;
private readonly internalListeners: Map<TokenListener, Unsubscribe> =
new Map();

Expand All @@ -51,6 +52,27 @@ export class AuthInterop implements FirebaseAuthInternal {
return { accessToken };
}

async getFirebaseToken(): Promise<{ accessToken: string } | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we use the existing getToken method instead of new? If we create new, then wouldn't the existing product logic have to make changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case we would have to force that getToken method should have forceRefresh as false if regional auth is initialized.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that would be the case as refreshToken is not supported. Alternatively we can log a warning message and ignore the field for now, but i would prefer throwing error as it makes it more clear.

this.assertAuthConfigured();
await this.auth._initializationPromise;
this.assertRegionalAuthConfigured();
if (!this.auth.firebaseToken) {
return null;
}

if (
!this.auth.firebaseToken.expirationTime ||
Date.now() >
this.auth.firebaseToken.expirationTime - this.TOKEN_EXPIRATION_BUFFER
) {
await this.auth._updateFirebaseToken(null);
return null;
}

const accessToken = await this.auth.firebaseToken.token;
return { accessToken };
}

addAuthTokenListener(listener: TokenListener): void {
this.assertAuthConfigured();
if (this.internalListeners.has(listener)) {
Expand Down Expand Up @@ -85,6 +107,10 @@ export class AuthInterop implements FirebaseAuthInternal {
);
}

private assertRegionalAuthConfigured(): void {
_assert(this.auth.tenantConfig, AuthErrorCode.OPERATION_NOT_ALLOWED);
}
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 required with new changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed unused methods.


private updateProactiveRefresh(): void {
if (this.internalListeners.size > 0) {
this.auth._startProactiveRefresh();
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: 10 }
);

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.
readonly expirationTime: number;
}

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