-
Notifications
You must be signed in to change notification settings - Fork 416
feat(appcheck): Add support for minting limited-use tokens and custom JTI #3027
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
base: main
Are you sure you want to change the base?
Changes from all commits
41c09c9
033568a
bae31aa
016b022
4433ba9
05586a3
0a1d94a
1f5d789
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ import { | |
| import { PrefixedFirebaseError } from '../utils/error'; | ||
| import * as utils from '../utils/index'; | ||
| import * as validator from '../utils/validator'; | ||
| import { AppCheckToken } from './app-check-api' | ||
| import { AppCheckToken, AppCheckTokenOptions } from './app-check-api' | ||
|
|
||
| // App Check backend constants | ||
| const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken'; | ||
|
|
@@ -58,7 +58,11 @@ export class AppCheckApiClient { | |
| * @param appId - The mobile App ID. | ||
| * @returns A promise that fulfills with a `AppCheckToken`. | ||
| */ | ||
| public exchangeToken(customToken: string, appId: string): Promise<AppCheckToken> { | ||
| public exchangeToken( | ||
| customToken: string, | ||
| appId: string, | ||
| options?: AppCheckTokenOptions | ||
| ): Promise<AppCheckToken> { | ||
| if (!validator.isNonEmptyString(appId)) { | ||
| throw new FirebaseAppCheckError( | ||
| 'invalid-argument', | ||
|
|
@@ -69,13 +73,34 @@ export class AppCheckApiClient { | |
| 'invalid-argument', | ||
| '`customToken` must be a non-empty string.'); | ||
| } | ||
| if (typeof options?.limitedUse !== 'undefined' && !validator.isBoolean(options.limitedUse)) { | ||
| throw new FirebaseAppCheckError( | ||
| 'invalid-argument', | ||
| '`limitedUse` must be a boolean value.'); | ||
| } | ||
| if (typeof options?.jti !== 'undefined') { | ||
| if (!validator.isString(options.jti)) { | ||
| throw new FirebaseAppCheckError( | ||
| 'invalid-argument', | ||
| '`jti` must be a string value.'); | ||
| } | ||
| if (options.jti !== '' && !options.limitedUse) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with JavaScript, but would it be possible to add a test case for leaving |
||
| throw new FirebaseAppCheckError( | ||
| 'invalid-argument', | ||
| '`jti` cannot be specified without setting `limitedUse` to `true`.'); | ||
| } | ||
| } | ||
| return this.getUrl(appId) | ||
| .then((url) => { | ||
| const request: HttpRequestConfig = { | ||
| method: 'POST', | ||
| url, | ||
| headers: FIREBASE_APP_CHECK_CONFIG_HEADERS, | ||
| data: { customToken } | ||
| data: { | ||
| customToken, | ||
| limitedUse: options?.limitedUse, | ||
| jti: options?.jti, | ||
| } | ||
| }; | ||
| return this.httpClient.send(request); | ||
| }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -210,11 +210,80 @@ describe('AppCheckApiClient', () => { | |
| method: 'POST', | ||
| url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, | ||
| headers: EXPECTED_HEADERS, | ||
| data: { customToken: TEST_TOKEN_TO_EXCHANGE } | ||
| data: { | ||
| customToken: TEST_TOKEN_TO_EXCHANGE, | ||
| limitedUse: undefined, | ||
| jti: undefined, | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| it('should resolve with the App Check token on success with limitedUse', () => { | ||
| const stub = sinon | ||
| .stub(HttpClient.prototype, 'send') | ||
| .resolves(utils.responseFrom(TEST_RESPONSE, 200)); | ||
| stubs.push(stub); | ||
| return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID, { limitedUse: true }) | ||
| .then((resp) => { | ||
| expect(resp.token).to.deep.equal(TEST_RESPONSE.token); | ||
| expect(resp.ttlMillis).to.deep.equal(3000); | ||
| expect(stub).to.have.been.calledOnce.and.calledWith({ | ||
| method: 'POST', | ||
| url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, | ||
| headers: EXPECTED_HEADERS, | ||
| data: { | ||
| customToken: TEST_TOKEN_TO_EXCHANGE, | ||
| limitedUse: true, | ||
| jti: undefined, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm, will the App Check backend see a JSON object with this |
||
| } | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| it('should resolve with the App Check token on success with jti', () => { | ||
| const stub = sinon | ||
| .stub(HttpClient.prototype, 'send') | ||
| .resolves(utils.responseFrom(TEST_RESPONSE, 200)); | ||
| stubs.push(stub); | ||
| return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID, { limitedUse: true, jti: 'test-jti' }) | ||
| .then((resp) => { | ||
| expect(resp.token).to.deep.equal(TEST_RESPONSE.token); | ||
| expect(resp.ttlMillis).to.deep.equal(3000); | ||
| expect(stub).to.have.been.calledOnce.and.calledWith({ | ||
| method: 'POST', | ||
| url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, | ||
| headers: EXPECTED_HEADERS, | ||
| data: { | ||
| customToken: TEST_TOKEN_TO_EXCHANGE, | ||
| limitedUse: true, | ||
| jti: 'test-jti', | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| const invalidJtis = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; | ||
| invalidJtis.forEach((invalidJti) => { | ||
| it('should throw given a non-string jti: ' + JSON.stringify(invalidJti), () => { | ||
| expect(() => { | ||
| apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID, { jti: invalidJti as any }); | ||
| }).to.throw('jti` must be a string value.'); | ||
| }); | ||
| }); | ||
|
|
||
| it('should throw given jti without limitedUse', () => { | ||
| expect(() => { | ||
| apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID, { jti: 'test-jti' }); | ||
| }).to.throw('jti` cannot be specified without setting `limitedUse` to `true`.'); | ||
| }); | ||
|
|
||
| it('should throw given jti with limitedUse set to false', () => { | ||
| expect(() => { | ||
| apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID, { jti: 'test-jti', limitedUse: false }); | ||
| }).to.throw('jti` cannot be specified without setting `limitedUse` to `true`.'); | ||
| }); | ||
|
|
||
| new Map([['3s', 3000], ['4.1s', 4100], ['3.000000001s', 3000], ['3.000001s', 3000]]) | ||
| .forEach((ttlMillis, ttlString) => { // value, key, map | ||
| // 3 seconds with 0 nanoseconds expressed as "3s" | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.