Skip to content

Commit

Permalink
feat(appcheck): Appcheck improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
lahirumaramba committed Apr 12, 2023
1 parent b7de8a1 commit 748dcec
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 2 deletions.
37 changes: 37 additions & 0 deletions src/app-check/app-check-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AppCheckToken } 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';
const ONE_TIME_USE_TOKEN_VERIFICATION_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}:verifyAppCheckToken';

const FIREBASE_APP_CHECK_CONFIG_HEADERS = {
'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`
Expand Down Expand Up @@ -86,6 +87,31 @@ export class AppCheckApiClient {
});
}

public verifyOneTimeProtection(token: string): Promise<boolean> {
if (!validator.isNonEmptyString(token)) {
throw new FirebaseAppCheckError(
'invalid-argument',
'`token` must be a non-empty string.');
}
return this.getVerifyTokenUrl()
.then((url) => {
const request: HttpRequestConfig = {
method: 'POST',
url,
headers: FIREBASE_APP_CHECK_CONFIG_HEADERS,
data: { token }
};
return this.httpClient.send(request);
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.then((resp) => {
return true;
})
.catch((err) => {
throw this.toFirebaseError(err);
});
}

private getUrl(appId: string): Promise<string> {
return this.getProjectId()
.then((projectId) => {
Expand All @@ -98,6 +124,17 @@ export class AppCheckApiClient {
});
}

private getVerifyTokenUrl(): Promise<string> {
return this.getProjectId()
.then((projectId) => {
const urlParams = {
projectId
};
const baseUrl = utils.formatString(ONE_TIME_USE_TOKEN_VERIFICATION_URL_FORMAT, urlParams);
return utils.formatString(baseUrl);
});
}

private getProjectId(): Promise<string> {
if (this.projectId) {
return Promise.resolve(this.projectId);
Expand Down
21 changes: 21 additions & 0 deletions src/app-check/app-check-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ export interface AppCheckTokenOptions {
ttlMillis?: number;
}

/**
* Interface representing options for {@link AppCheck.verifyToken} method.
*/
export interface VerifyAppCheckTokenOptions {
/**
* Sets the one-time use tokens feature.
* When set to `true`, checks if this token has already been consumed.
* This feature requires an additional network call to the backend and could be slower when enabled.
*/
consume?: boolean;
}

/**
* Interface representing a decoded Firebase App Check token, returned from the
* {@link AppCheck.verifyToken} method.
Expand Down Expand Up @@ -86,6 +98,15 @@ export interface DecodedAppCheckToken {
* convenience, and is set as the value of the {@link DecodedAppCheckToken.sub | sub} property.
*/
app_id: string;

/**
* Indicates weather this token was already consumed.
* If this is the first time {@link AppCheck.verifyToken} method has seen this token,
* this field will contain the value `false`. The given token will then be
* marked as `already_consumed` for all future invocations of this {@link AppCheck.verifyToken}
* method for this token.
*/
already_consumed?: boolean;
[key: string]: any;
}

Expand Down
31 changes: 29 additions & 2 deletions src/app-check/app-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
* limitations under the License.
*/

import * as validator from '../utils/validator';

import { App } from '../app';
import { AppCheckApiClient } from './app-check-api-client-internal';
import { AppCheckApiClient, FirebaseAppCheckError } from './app-check-api-client-internal';
import {
appCheckErrorFromCryptoSignerError, AppCheckTokenGenerator,
} from './token-generator';
Expand All @@ -26,6 +28,7 @@ import { cryptoSignerFromApp } from '../utils/crypto-signer';
import {
AppCheckToken,
AppCheckTokenOptions,
VerifyAppCheckTokenOptions,
VerifyAppCheckTokenResponse,
} from './app-check-api';

Expand Down Expand Up @@ -75,17 +78,41 @@ export class AppCheck {
* rejected.
*
* @param appCheckToken - The App Check token to verify.
* @param options - Optional {@link VerifyAppCheckTokenOptions} object when verifying an App Check Token.
*
* @returns A promise fulfilled with the token's decoded claims
* if the App Check token is valid; otherwise, a rejected promise.
*/
public verifyToken(appCheckToken: string): Promise<VerifyAppCheckTokenResponse> {
public verifyToken(appCheckToken: string, options?: VerifyAppCheckTokenOptions)
: Promise<VerifyAppCheckTokenResponse> {
this.validateVerifyAppCheckTokenOptions(options);
return this.appCheckTokenVerifier.verifyToken(appCheckToken)
.then((decodedToken) => {
if (options?.consume) {
return this.client.verifyOneTimeProtection(appCheckToken)
.then((alreadyConsumed) => {
decodedToken.already_consumed = alreadyConsumed;
return {
appId: decodedToken.app_id,
token: decodedToken,
};
});
}
return {
appId: decodedToken.app_id,
token: decodedToken,
};
});
}

private validateVerifyAppCheckTokenOptions(options?: VerifyAppCheckTokenOptions): void {
if (typeof options === 'undefined') {
return;
}
if (!validator.isNonNullObject(options)) {
throw new FirebaseAppCheckError(
'invalid-argument',
'VerifyAppCheckTokenOptions must be a non-null object.');
}
}
}

0 comments on commit 748dcec

Please sign in to comment.