Skip to content

Commit 748dcec

Browse files
committed
feat(appcheck): Appcheck improvements
1 parent b7de8a1 commit 748dcec

File tree

3 files changed

+87
-2
lines changed

3 files changed

+87
-2
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { AppCheckToken } from './app-check-api'
2727

2828
// App Check backend constants
2929
const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken';
30+
const ONE_TIME_USE_TOKEN_VERIFICATION_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}:verifyAppCheckToken';
3031

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

90+
public verifyOneTimeProtection(token: string): Promise<boolean> {
91+
if (!validator.isNonEmptyString(token)) {
92+
throw new FirebaseAppCheckError(
93+
'invalid-argument',
94+
'`token` must be a non-empty string.');
95+
}
96+
return this.getVerifyTokenUrl()
97+
.then((url) => {
98+
const request: HttpRequestConfig = {
99+
method: 'POST',
100+
url,
101+
headers: FIREBASE_APP_CHECK_CONFIG_HEADERS,
102+
data: { token }
103+
};
104+
return this.httpClient.send(request);
105+
})
106+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
107+
.then((resp) => {
108+
return true;
109+
})
110+
.catch((err) => {
111+
throw this.toFirebaseError(err);
112+
});
113+
}
114+
89115
private getUrl(appId: string): Promise<string> {
90116
return this.getProjectId()
91117
.then((projectId) => {
@@ -98,6 +124,17 @@ export class AppCheckApiClient {
98124
});
99125
}
100126

127+
private getVerifyTokenUrl(): Promise<string> {
128+
return this.getProjectId()
129+
.then((projectId) => {
130+
const urlParams = {
131+
projectId
132+
};
133+
const baseUrl = utils.formatString(ONE_TIME_USE_TOKEN_VERIFICATION_URL_FORMAT, urlParams);
134+
return utils.formatString(baseUrl);
135+
});
136+
}
137+
101138
private getProjectId(): Promise<string> {
102139
if (this.projectId) {
103140
return Promise.resolve(this.projectId);

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ export interface AppCheckTokenOptions {
4141
ttlMillis?: number;
4242
}
4343

44+
/**
45+
* Interface representing options for {@link AppCheck.verifyToken} method.
46+
*/
47+
export interface VerifyAppCheckTokenOptions {
48+
/**
49+
* Sets the one-time use tokens feature.
50+
* When set to `true`, checks if this token has already been consumed.
51+
* This feature requires an additional network call to the backend and could be slower when enabled.
52+
*/
53+
consume?: boolean;
54+
}
55+
4456
/**
4557
* Interface representing a decoded Firebase App Check token, returned from the
4658
* {@link AppCheck.verifyToken} method.
@@ -86,6 +98,15 @@ export interface DecodedAppCheckToken {
8698
* convenience, and is set as the value of the {@link DecodedAppCheckToken.sub | sub} property.
8799
*/
88100
app_id: string;
101+
102+
/**
103+
* Indicates weather this token was already consumed.
104+
* If this is the first time {@link AppCheck.verifyToken} method has seen this token,
105+
* this field will contain the value `false`. The given token will then be
106+
* marked as `already_consumed` for all future invocations of this {@link AppCheck.verifyToken}
107+
* method for this token.
108+
*/
109+
already_consumed?: boolean;
89110
[key: string]: any;
90111
}
91112

src/app-check/app-check.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
* limitations under the License.
1616
*/
1717

18+
import * as validator from '../utils/validator';
19+
1820
import { App } from '../app';
19-
import { AppCheckApiClient } from './app-check-api-client-internal';
21+
import { AppCheckApiClient, FirebaseAppCheckError } from './app-check-api-client-internal';
2022
import {
2123
appCheckErrorFromCryptoSignerError, AppCheckTokenGenerator,
2224
} from './token-generator';
@@ -26,6 +28,7 @@ import { cryptoSignerFromApp } from '../utils/crypto-signer';
2628
import {
2729
AppCheckToken,
2830
AppCheckTokenOptions,
31+
VerifyAppCheckTokenOptions,
2932
VerifyAppCheckTokenResponse,
3033
} from './app-check-api';
3134

@@ -75,17 +78,41 @@ export class AppCheck {
7578
* rejected.
7679
*
7780
* @param appCheckToken - The App Check token to verify.
81+
* @param options - Optional {@link VerifyAppCheckTokenOptions} object when verifying an App Check Token.
7882
*
7983
* @returns A promise fulfilled with the token's decoded claims
8084
* if the App Check token is valid; otherwise, a rejected promise.
8185
*/
82-
public verifyToken(appCheckToken: string): Promise<VerifyAppCheckTokenResponse> {
86+
public verifyToken(appCheckToken: string, options?: VerifyAppCheckTokenOptions)
87+
: Promise<VerifyAppCheckTokenResponse> {
88+
this.validateVerifyAppCheckTokenOptions(options);
8389
return this.appCheckTokenVerifier.verifyToken(appCheckToken)
8490
.then((decodedToken) => {
91+
if (options?.consume) {
92+
return this.client.verifyOneTimeProtection(appCheckToken)
93+
.then((alreadyConsumed) => {
94+
decodedToken.already_consumed = alreadyConsumed;
95+
return {
96+
appId: decodedToken.app_id,
97+
token: decodedToken,
98+
};
99+
});
100+
}
85101
return {
86102
appId: decodedToken.app_id,
87103
token: decodedToken,
88104
};
89105
});
90106
}
107+
108+
private validateVerifyAppCheckTokenOptions(options?: VerifyAppCheckTokenOptions): void {
109+
if (typeof options === 'undefined') {
110+
return;
111+
}
112+
if (!validator.isNonNullObject(options)) {
113+
throw new FirebaseAppCheckError(
114+
'invalid-argument',
115+
'VerifyAppCheckTokenOptions must be a non-null object.');
116+
}
117+
}
91118
}

0 commit comments

Comments
 (0)