Skip to content

Commit f8410db

Browse files
committed
Implement exchangeToken public api
1 parent 212b177 commit f8410db

File tree

12 files changed

+434
-33
lines changed

12 files changed

+434
-33
lines changed

common/api-review/auth.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ export interface EmulatorConfig {
364364

365365
export { ErrorFn }
366366

367+
// @public (undocumented)
368+
export function exchangeToken(auth: Auth, idpConfigId: string, customToken: string): Promise<string>;
369+
367370
// Warning: (ae-forgotten-export) The symbol "BaseOAuthProvider" needs to be exported by the entry point index.d.ts
368371
//
369372
// @public

docs-devsite/auth.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Firebase Authentication
2828
| [confirmPasswordReset(auth, oobCode, newPassword)](./auth.md#confirmpasswordreset_749dad8) | Completes the password reset process, given a confirmation code and new password. |
2929
| [connectAuthEmulator(auth, url, options)](./auth.md#connectauthemulator_657c7e5) | Changes the [Auth](./auth.auth.md#auth_interface) instance to communicate with the Firebase Auth Emulator, instead of production Firebase Auth services. |
3030
| [createUserWithEmailAndPassword(auth, email, password)](./auth.md#createuserwithemailandpassword_21ad33b) | Creates a new user account associated with the specified email address and password. |
31+
| [exchangeToken(auth, idpConfigId, customToken)](./auth.md#exchangetoken_b6b1871) | Asynchronously exchanges an OIDC provider's Authorization code or Id Token for an OidcToken i.e. Outbound Access Token. |
3132
| [fetchSignInMethodsForEmail(auth, email)](./auth.md#fetchsigninmethodsforemail_efb3887) | Gets the list of possible sign in methods for the given email address. This method returns an empty list when [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) is enabled, irrespective of the number of authentication methods available for the given email. |
3233
| [getMultiFactorResolver(auth, error)](./auth.md#getmultifactorresolver_201ba61) | Provides a [MultiFactorResolver](./auth.multifactorresolver.md#multifactorresolver_interface) suitable for completion of a multi-factor flow. |
3334
| [getRedirectResult(auth, resolver)](./auth.md#getredirectresult_c35dc1f) | Returns a [UserCredential](./auth.usercredential.md#usercredential_interface) from the redirect-based sign-in flow. |
@@ -405,6 +406,34 @@ export declare function createUserWithEmailAndPassword(auth: Auth, email: string
405406

406407
Promise&lt;[UserCredential](./auth.usercredential.md#usercredential_interface)<!-- -->&gt;
407408

409+
### exchangeToken(auth, idpConfigId, customToken) {:#exchangetoken_b6b1871}
410+
411+
Asynchronously exchanges an OIDC provider's Authorization code or Id Token for an OidcToken i.e. Outbound Access Token.
412+
413+
This method is imeplemented only for and requires [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface) to be configured in the [Auth](./auth.auth.md#auth_interface) instance used.
414+
415+
Fails with an error if the token is invalid, expired, or not accepted by the Firebase Auth service.
416+
417+
<b>Signature:</b>
418+
419+
```typescript
420+
export declare function exchangeToken(auth: Auth, idpConfigId: string, customToken: string): Promise<string>;
421+
```
422+
423+
#### Parameters
424+
425+
| Parameter | Type | Description |
426+
| --- | --- | --- |
427+
| auth | [Auth](./auth.auth.md#auth_interface) | The [Auth](./auth.auth.md#auth_interface) instance. |
428+
| idpConfigId | string | The ExternalUserDirectoryId corresponding to the OIDC custom Token. |
429+
| customToken | string | The OIDC provider's Authorization code or Id Token to exchange. |
430+
431+
<b>Returns:</b>
432+
433+
Promise&lt;string&gt;
434+
435+
The firebase access token (JWT signed by Firebase Auth).
436+
408437
### fetchSignInMethodsForEmail(auth, email) {:#fetchsigninmethodsforemail_efb3887}
409438

410439
Gets the list of possible sign in methods for the given email address. This method returns an empty list when [Email Enumeration Protection](https://cloud.google.com/identity-platform/docs/admin/email-enumeration-protection) is enabled, irrespective of the number of authentication methods available for the given email.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import chaiAsPromised from 'chai-as-promised';
20+
21+
import {
22+
regionalTestAuth,
23+
testAuth,
24+
TestAuth
25+
} from '../../../test/helpers/mock_auth';
26+
import * as mockFetch from '../../../test/helpers/mock_fetch';
27+
import { mockRegionalEndpointWithParent } from '../../../test/helpers/api/helper';
28+
import { exchangeToken } from './exchange_token';
29+
import { HttpHeader, RegionalEndpoint } from '..';
30+
import { FirebaseError } from '@firebase/util';
31+
import { ServerError } from '../errors';
32+
33+
use(chaiAsPromised);
34+
35+
describe('api/authentication/exchange_token', () => {
36+
let auth: TestAuth;
37+
let regionalAuth: TestAuth;
38+
const request = {
39+
parent: 'test-parent',
40+
token: 'custom-token'
41+
};
42+
43+
beforeEach(async () => {
44+
auth = await testAuth();
45+
regionalAuth = await regionalTestAuth();
46+
mockFetch.setUp();
47+
});
48+
49+
afterEach(mockFetch.tearDown);
50+
51+
it('returns accesss token for Regional Auth', async () => {
52+
const mock = mockRegionalEndpointWithParent(
53+
RegionalEndpoint.EXCHANGE_TOKEN,
54+
'test-parent',
55+
{ accessToken: 'outbound-token' }
56+
);
57+
58+
const response = await exchangeToken(regionalAuth, request);
59+
expect(response.accessToken).equal('outbound-token');
60+
expect(mock.calls[0].request).to.eql({
61+
parent: 'test-parent',
62+
token: 'custom-token'
63+
});
64+
expect(mock.calls[0].method).to.eq('POST');
65+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
66+
'application/json'
67+
);
68+
});
69+
70+
it('throws exception for default Auth', async () => {
71+
await expect(exchangeToken(auth, request)).to.be.rejectedWith(
72+
FirebaseError,
73+
'Firebase: Operations not allowed for the auth object initialized. (auth/operation-not-allowed).'
74+
);
75+
});
76+
77+
it('should handle errors', async () => {
78+
const mock = mockRegionalEndpointWithParent(
79+
RegionalEndpoint.EXCHANGE_TOKEN,
80+
'test-parent',
81+
{
82+
error: {
83+
code: 400,
84+
message: ServerError.INVALID_CUSTOM_TOKEN,
85+
errors: [
86+
{
87+
message: ServerError.INVALID_CUSTOM_TOKEN
88+
}
89+
]
90+
}
91+
},
92+
400
93+
);
94+
95+
await expect(exchangeToken(regionalAuth, request)).to.be.rejectedWith(
96+
FirebaseError,
97+
'(auth/invalid-custom-token).'
98+
);
99+
expect(mock.calls[0].request).to.eql(request);
100+
});
101+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import {
18+
RegionalEndpoint,
19+
HttpMethod,
20+
_performRegionalApiRequest
21+
} from '../index';
22+
import { Auth } from '../../model/public_types';
23+
24+
export interface ExchangeTokenRequest {
25+
parent: string;
26+
token: string;
27+
}
28+
29+
export interface ExchangeTokenRespose {
30+
accessToken: string;
31+
}
32+
33+
export async function exchangeToken(
34+
auth: Auth,
35+
request: ExchangeTokenRequest
36+
): Promise<ExchangeTokenRespose> {
37+
return _performRegionalApiRequest<ExchangeTokenRequest, ExchangeTokenRespose>(
38+
auth,
39+
HttpMethod.POST,
40+
RegionalEndpoint.EXCHANGE_TOKEN,
41+
request,
42+
{},
43+
request.parent
44+
);
45+
}

packages/auth/src/api/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { ConfigInternal } from '../model/auth';
3636
import {
3737
_getFinalTarget,
3838
_performApiRequest,
39+
_performRegionalApiRequest,
3940
DEFAULT_API_TIMEOUT_MS,
4041
Endpoint,
4142
RegionalEndpoint,
@@ -606,7 +607,7 @@ describe('api/_performApiRequest', () => {
606607
context('throws Operation not allowed exception', () => {
607608
it('when tenantConfig is not initialized and Regional Endpoint is used', async () => {
608609
await expect(
609-
_performApiRequest<typeof request, typeof serverResponse>(
610+
_performRegionalApiRequest<typeof request, typeof serverResponse>(
610611
auth,
611612
HttpMethod.POST,
612613
RegionalEndpoint.EXCHANGE_TOKEN,

packages/auth/src/api/index.ts

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export const enum HttpHeader {
5454
X_FIREBASE_APP_CHECK = 'X-Firebase-AppCheck'
5555
}
5656

57-
export enum Endpoint {
57+
export const enum Endpoint {
5858
CREATE_AUTH_URI = '/v1/accounts:createAuthUri',
5959
DELETE_ACCOUNT = '/v1/accounts:delete',
6060
RESET_PASSWORD = '/v1/accounts:resetPassword',
@@ -81,8 +81,11 @@ export enum Endpoint {
8181
REVOKE_TOKEN = '/v2/accounts:revokeToken'
8282
}
8383

84-
export enum RegionalEndpoint {
85-
EXCHANGE_TOKEN = 'v2/${body.parent}:exchangeOidcToken'
84+
export const EXCHANGE_TOKEN_PARENT =
85+
'projects/${projectId}/locations/${location}/tenants/${tenantId}/idpConfigs/${idpConfigId}';
86+
87+
export const enum RegionalEndpoint {
88+
EXCHANGE_TOKEN = ':exchangeOidcToken'
8689
}
8790

8891
const CookieAuthProxiedEndpoints: string[] = [
@@ -141,14 +144,17 @@ export function _addTidIfNecessary<T extends { tenantId?: string }>(
141144
return request;
142145
}
143146

144-
export async function _performApiRequest<T, V>(
147+
function isRegionalAuthInitialized(auth: Auth): boolean {
148+
return !!auth.tenantConfig;
149+
}
150+
151+
async function performApiRequest<T, V>(
145152
auth: Auth,
146153
method: HttpMethod,
147-
path: Endpoint | RegionalEndpoint,
154+
path: string,
148155
request?: T,
149156
customErrorMap: Partial<ServerErrorMap<ServerError>> = {}
150157
): Promise<V> {
151-
_assertValidEndpointForAuth(auth, path);
152158
return _performFetchWithErrorHandling(auth, customErrorMap, async () => {
153159
let body = {};
154160
let params = {};
@@ -162,10 +168,17 @@ export async function _performApiRequest<T, V>(
162168
}
163169
}
164170

165-
const query = querystring({
166-
key: auth.config.apiKey,
167-
...params
168-
}).slice(1);
171+
let queryParamString: string;
172+
if (isRegionalAuthInitialized(auth)) {
173+
queryParamString = querystring({
174+
...params
175+
}).slice(1);
176+
} else {
177+
queryParamString = querystring({
178+
key: auth.config.apiKey,
179+
...params
180+
}).slice(1);
181+
}
169182

170183
const headers = await (auth as AuthInternal)._getAdditionalHeaders();
171184
headers[HttpHeader.CONTENT_TYPE] = 'application/json';
@@ -193,12 +206,45 @@ export async function _performApiRequest<T, V>(
193206
}
194207

195208
return FetchProvider.fetch()(
196-
await _getFinalTarget(auth, auth.config.apiHost, path, query),
209+
await _getFinalTarget(auth, auth.config.apiHost, path, queryParamString),
197210
fetchArgs
198211
);
199212
});
200213
}
201214

215+
export async function _performRegionalApiRequest<T, V>(
216+
auth: Auth,
217+
method: HttpMethod,
218+
path: RegionalEndpoint,
219+
request?: T,
220+
customErrorMap: Partial<ServerErrorMap<ServerError>> = {},
221+
parent?: string
222+
): Promise<V> {
223+
if (!isRegionalAuthInitialized(auth)) {
224+
throw _operationNotSupportedForInitializedAuthInstance(auth);
225+
}
226+
return performApiRequest(
227+
auth,
228+
method,
229+
`${parent}${path}`,
230+
request,
231+
customErrorMap
232+
);
233+
}
234+
235+
export async function _performApiRequest<T, V>(
236+
auth: Auth,
237+
method: HttpMethod,
238+
path: Endpoint,
239+
request?: T,
240+
customErrorMap: Partial<ServerErrorMap<ServerError>> = {}
241+
): Promise<V> {
242+
if (isRegionalAuthInitialized(auth)) {
243+
throw _operationNotSupportedForInitializedAuthInstance(auth);
244+
}
245+
return performApiRequest(auth, method, `${path}`, request, customErrorMap);
246+
}
247+
202248
export async function _performFetchWithErrorHandling<V>(
203249
auth: Auth,
204250
customErrorMap: Partial<ServerErrorMap<ServerError>>,
@@ -287,9 +333,9 @@ export async function _getFinalTarget(
287333
auth: Auth,
288334
host: string,
289335
path: string,
290-
query: string
336+
query?: string
291337
): Promise<string> {
292-
const base = `${host}${path}?${query}`;
338+
const base = query ? `${host}${path}?${query}` : `${host}${path}`;
293339

294340
const authInternal = auth as AuthInternal;
295341
const finalTarget = authInternal.config.emulator
@@ -328,22 +374,6 @@ export function _parseEnforcementState(
328374
}
329375
}
330376

331-
function _assertValidEndpointForAuth(
332-
auth: Auth,
333-
path: Endpoint | RegionalEndpoint
334-
): void {
335-
if (
336-
!auth.tenantConfig &&
337-
Object.values(RegionalEndpoint).includes(path as RegionalEndpoint)
338-
) {
339-
throw _operationNotSupportedForInitializedAuthInstance(auth);
340-
}
341-
342-
if (auth.tenantConfig && Object.values(Endpoint).includes(path as Endpoint)) {
343-
throw _operationNotSupportedForInitializedAuthInstance(auth);
344-
}
345-
}
346-
347377
class NetworkTimeout<T> {
348378
// Node timers and browser timers are fundamentally incompatible, but we
349379
// don't care about the value here

packages/auth/src/core/auth/auth_impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const enum DefaultConfig {
9393
TOKEN_API_HOST = 'securetoken.googleapis.com',
9494
API_HOST = 'identitytoolkit.googleapis.com',
9595
API_SCHEME = 'https',
96-
REGIONAL_API_HOST = 'identityplatform.googleapis.com'
96+
REGIONAL_API_HOST = 'identityplatform.googleapis.com/v2alpha/'
9797
}
9898

9999
export class AuthImpl implements AuthInternal, _FirebaseService {

packages/auth/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ export {
315315
sendEmailVerification,
316316
verifyBeforeUpdateEmail
317317
} from './strategies/email';
318+
export { exchangeToken } from './strategies/exhange_token';
318319

319320
// core
320321
export { ActionCodeURL, parseActionCodeURL } from './action_code_url';

0 commit comments

Comments
 (0)