Skip to content

Commit f142d51

Browse files
committed
feat(appcheck): Add support for minting limited use tokens
1 parent 179dab7 commit f142d51

File tree

7 files changed

+76
-5
lines changed

7 files changed

+76
-5
lines changed

etc/firebase-admin.app-check.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface AppCheckToken {
2424

2525
// @public
2626
export interface AppCheckTokenOptions {
27+
limitedUse?: boolean;
2728
ttlMillis?: number;
2829
}
2930

src/app-check/app-check-api-client-internal.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ export class AppCheckApiClient {
5858
* @param appId - The mobile App ID.
5959
* @returns A promise that fulfills with a `AppCheckToken`.
6060
*/
61-
public exchangeToken(customToken: string, appId: string): Promise<AppCheckToken> {
61+
public exchangeToken(
62+
customToken: string,
63+
appId: string,
64+
limitedUse?: boolean
65+
): Promise<AppCheckToken> {
6266
if (!validator.isNonEmptyString(appId)) {
6367
throw new FirebaseAppCheckError(
6468
'invalid-argument',
@@ -75,7 +79,10 @@ export class AppCheckApiClient {
7579
method: 'POST',
7680
url,
7781
headers: FIREBASE_APP_CHECK_CONFIG_HEADERS,
78-
data: { customToken }
82+
data: {
83+
customToken,
84+
limitedUse,
85+
}
7986
};
8087
return this.httpClient.send(request);
8188
})

src/app-check/app-check-api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export interface AppCheckTokenOptions {
3939
* be valid. This value must be between 30 minutes and 7 days, inclusive.
4040
*/
4141
ttlMillis?: number;
42+
43+
/**
44+
* Specifies whether this attestation is for use in a *limited use* (`true`)
45+
* or *session based* (`false`) context. To enable this attestation to be used
46+
* with the *replay protection* feature, set this to `true`. The default value
47+
* is `false`.
48+
*/
49+
limitedUse?: boolean;
4250
}
4351

4452
/**

src/app-check/app-check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class AppCheck {
6868
public createToken(appId: string, options?: AppCheckTokenOptions): Promise<AppCheckToken> {
6969
return this.tokenGenerator.createCustomToken(appId, options)
7070
.then((customToken) => {
71-
return this.client.exchangeToken(customToken, appId);
71+
return this.client.exchangeToken(customToken, appId, options?.limitedUse);
7272
});
7373
}
7474

test/integration/app-check.spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,20 @@ describe('admin.appCheck', () => {
5353
expect(token).to.have.keys(['token', 'ttlMillis']);
5454
expect(token.token).to.be.a('string').and.to.not.be.empty;
5555
expect(token.ttlMillis).to.be.a('number');
56-
expect(token.ttlMillis).to.equals(3600000);
56+
expect(token.ttlMillis).to.equals(3600000); // 1 hour
57+
});
58+
});
59+
60+
it('should succeed with a vaild limited use token', function () {
61+
if (!appId) {
62+
this.skip();
63+
}
64+
return admin.appCheck().createToken(appId as string, { limitedUse: true })
65+
.then((token) => {
66+
expect(token).to.have.keys(['token', 'ttlMillis']);
67+
expect(token.token).to.be.a('string').and.to.not.be.empty;
68+
expect(token.ttlMillis).to.be.a('number');
69+
expect(token.ttlMillis).to.equals(300000); // 5 minutes
5770
});
5871
});
5972

test/unit/app-check/app-check-api-client-internal.spec.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as _ from 'lodash';
2121
import * as chai from 'chai';
2222
import * as sinon from 'sinon';
2323
import { HttpClient } from '../../../src/utils/api-request';
24+
import * as sinonChai from 'sinon-chai';
2425
import * as utils from '../utils';
2526
import * as mocks from '../../resources/mocks';
2627
import { getMetricsHeader, getSdkVersion } from '../../../src/utils';
@@ -31,6 +32,7 @@ import { FirebaseAppError } from '../../../src/utils/error';
3132
import { deepCopy } from '../../../src/utils/deep-copy';
3233

3334
const expect = chai.expect;
35+
chai.use(sinonChai);
3436

3537
describe('AppCheckApiClient', () => {
3638

@@ -210,7 +212,31 @@ describe('AppCheckApiClient', () => {
210212
method: 'POST',
211213
url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`,
212214
headers: EXPECTED_HEADERS,
213-
data: { customToken: TEST_TOKEN_TO_EXCHANGE }
215+
data: {
216+
customToken: TEST_TOKEN_TO_EXCHANGE,
217+
limitedUse: undefined,
218+
}
219+
});
220+
});
221+
});
222+
223+
it('should resolve with the App Check token on success with limitedUse', () => {
224+
const stub = sinon
225+
.stub(HttpClient.prototype, 'send')
226+
.resolves(utils.responseFrom(TEST_RESPONSE, 200));
227+
stubs.push(stub);
228+
return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID, true)
229+
.then((resp) => {
230+
expect(resp.token).to.deep.equal(TEST_RESPONSE.token);
231+
expect(resp.ttlMillis).to.deep.equal(3000);
232+
expect(stub).to.have.been.calledOnce.and.calledWith({
233+
method: 'POST',
234+
url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`,
235+
headers: EXPECTED_HEADERS,
236+
data: {
237+
customToken: TEST_TOKEN_TO_EXCHANGE,
238+
limitedUse: true,
239+
}
214240
});
215241
});
216242
});

test/unit/app-check/app-check.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import * as _ from 'lodash';
2121
import * as chai from 'chai';
2222
import * as sinon from 'sinon';
23+
import * as sinonChai from 'sinon-chai';
2324
import * as mocks from '../../resources/mocks';
2425

2526
import { FirebaseApp } from '../../../src/app/firebase-app';
@@ -31,6 +32,7 @@ import { ServiceAccountSigner } from '../../../src/utils/crypto-signer';
3132
import { AppCheckTokenVerifier } from '../../../src/app-check/token-verifier';
3233

3334
const expect = chai.expect;
35+
chai.use(sinonChai);
3436

3537
describe('AppCheck', () => {
3638

@@ -168,6 +170,20 @@ describe('AppCheck', () => {
168170
expect(token.ttlMillis).equals(3000);
169171
});
170172
});
173+
174+
it('should resolve with AppCheckToken on success with limitedUse', () => {
175+
const response = { token: 'token', ttlMillis: 3000 };
176+
const stub = sinon
177+
.stub(AppCheckApiClient.prototype, 'exchangeToken')
178+
.resolves(response);
179+
stubs.push(stub);
180+
return appCheck.createToken(APP_ID, { limitedUse: true })
181+
.then((token) => {
182+
expect(token.token).equals('token');
183+
expect(token.ttlMillis).equals(3000);
184+
expect(stub).to.have.been.calledOnce.and.calledWith(sinon.match.string, APP_ID, true);
185+
});
186+
});
171187
});
172188

173189
describe('verifyToken', () => {

0 commit comments

Comments
 (0)