Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions etc/firebase-admin.app-check.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface AppCheckToken {

// @public
export interface AppCheckTokenOptions {
limitedUse?: boolean;
ttlMillis?: number;
}

Expand Down
11 changes: 9 additions & 2 deletions src/app-check/app-check-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
limitedUse?: boolean
): Promise<AppCheckToken> {
Comment on lines +61 to +65

Choose a reason for hiding this comment

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

medium

For improved robustness and a better developer experience, it would be beneficial to add input validation for the limitedUse parameter. This ensures that if a value is provided, it must be a boolean. This is consistent with how other parameters like appId and customToken are validated within this method, and it provides clearer error messages to developers for invalid inputs.

You can add the following check at the beginning of the function body:

if (typeof limitedUse !== 'undefined' && !validator.isBoolean(limitedUse)) {
  throw new FirebaseAppCheckError(
    'invalid-argument',
    '`limitedUse` must be a boolean value.');
}

if (!validator.isNonEmptyString(appId)) {
throw new FirebaseAppCheckError(
'invalid-argument',
Expand All @@ -75,7 +79,10 @@ export class AppCheckApiClient {
method: 'POST',
url,
headers: FIREBASE_APP_CHECK_CONFIG_HEADERS,
data: { customToken }
data: {
customToken,
limitedUse,
}
};
return this.httpClient.send(request);
})
Expand Down
7 changes: 7 additions & 0 deletions src/app-check/app-check-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export interface AppCheckTokenOptions {
* be valid. This value must be between 30 minutes and 7 days, inclusive.
*/
ttlMillis?: number;

/**
* Specifies whether this token is for a limited use context.
* To enable this token to be used with the replay protection feature, set this to `true`.
* The default value is `false`.
*/
limitedUse?: boolean;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/app-check/app-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class AppCheck {
public createToken(appId: string, options?: AppCheckTokenOptions): Promise<AppCheckToken> {
return this.tokenGenerator.createCustomToken(appId, options)
.then((customToken) => {
return this.client.exchangeToken(customToken, appId);
return this.client.exchangeToken(customToken, appId, options?.limitedUse);
});
}

Expand Down
15 changes: 14 additions & 1 deletion test/integration/app-check.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,20 @@ describe('admin.appCheck', () => {
expect(token).to.have.keys(['token', 'ttlMillis']);
expect(token.token).to.be.a('string').and.to.not.be.empty;
expect(token.ttlMillis).to.be.a('number');
expect(token.ttlMillis).to.equals(3600000);
expect(token.ttlMillis).to.equals(3600000); // 1 hour
});
});

it('should succeed with a vaild limited use token', function () {
if (!appId) {
this.skip();
}
return admin.appCheck().createToken(appId as string, { limitedUse: true })
.then((token) => {
expect(token).to.have.keys(['token', 'ttlMillis']);
expect(token.token).to.be.a('string').and.to.not.be.empty;
expect(token.ttlMillis).to.be.a('number');
expect(token.ttlMillis).to.equals(300000); // 5 minutes
});
});

Expand Down
26 changes: 25 additions & 1 deletion test/unit/app-check/app-check-api-client-internal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,31 @@ 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,
}
});
});
});

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, 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,
}
});
});
});
Expand Down
14 changes: 14 additions & 0 deletions test/unit/app-check/app-check.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,20 @@ describe('AppCheck', () => {
expect(token.ttlMillis).equals(3000);
});
});

it('should resolve with AppCheckToken on success with limitedUse', () => {
const response = { token: 'token', ttlMillis: 3000 };
const stub = sinon
.stub(AppCheckApiClient.prototype, 'exchangeToken')
.resolves(response);
stubs.push(stub);
return appCheck.createToken(APP_ID, { limitedUse: true })
.then((token) => {
expect(token.token).equals('token');
expect(token.ttlMillis).equals(3000);
expect(stub).to.have.been.calledOnce.and.calledWith(sinon.match.string, APP_ID, true);
});
});
});

describe('verifyToken', () => {
Expand Down