Skip to content
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

Add App Check token to FirebaseServerApp #8651

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open

Conversation

DellaBitta
Copy link
Contributor

@DellaBitta DellaBitta commented Nov 22, 2024

Discussion

Currently a customer enforces App Check for a product in the Firebase console then there's no way to successfully use that product in SSR environments. This is due to the requirement for products SDKs to internally invoke getToken on the a instance of the App Check SDK that the customer's app has initialized. However, the App Check SDK can only be initialized in browser environments.

This PR aims to fix this. FirebaseServerApp will now accept an optional App Check token at initialization. The product SDKs will look for this token, and if it's present, the SDKs will use this value in lieu of calling getToken on App Check.

This change affects the following SDKs:

  • Auth
  • Cloud Functions
  • Data Connect
  • Firestore
  • Real Time Database
  • Vertex AI

Testing

Added tests to FirebaseServerApp's handling of the token. The product SDKs currently don't have viable App Check tests in our test suite, so I manually tested all of the SDKs by enabling App Check enforcement in the Firebase Console, watching them fail due to permissions, and then provided the App Check token to Firebase Server app and watched them succeed.

API Changes

This conforms to the previoulsy approved Existing FirebaseServerApp API proposal (internal) for FirebaseServerApp which includes App Check token support.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Nov 22, 2024

Size Report 1

Affected Products

  • @firebase/app

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser18.4 kB19.4 kB+977 B (+5.3%)
    main19.3 kB20.2 kB+969 B (+5.0%)
    module18.4 kB19.4 kB+977 B (+5.3%)
  • @firebase/auth

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser188 kB188 kB+107 B (+0.1%)
    cordova164 kB164 kB+107 B (+0.1%)
    main145 kB145 kB+111 B (+0.1%)
    module188 kB188 kB+107 B (+0.1%)
    react-native163 kB163 kB+113 B (+0.1%)
  • @firebase/auth-cordova

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser164 kB164 kB+107 B (+0.1%)
    module164 kB164 kB+107 B (+0.1%)
  • @firebase/auth-web-extension

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser140 kB140 kB+107 B (+0.1%)
    main157 kB157 kB+113 B (+0.1%)
    module140 kB140 kB+107 B (+0.1%)
  • @firebase/auth/internal

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser199 kB199 kB+107 B (+0.1%)
    main171 kB171 kB+113 B (+0.1%)
    module199 kB199 kB+107 B (+0.1%)
  • @firebase/data-connect

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser20.2 kB20.4 kB+157 B (+0.8%)
    main22.1 kB22.3 kB+148 B (+0.7%)
    module20.2 kB20.4 kB+157 B (+0.8%)
  • @firebase/database

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser249 kB249 kB+332 B (+0.1%)
    main254 kB254 kB+325 B (+0.1%)
    module249 kB249 kB+332 B (+0.1%)
  • @firebase/database-compat/standalone

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    main340 kB366 kB+26.0 kB (+7.7%)
  • @firebase/firestore

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser381 kB382 kB+563 B (+0.1%)
    main588 kB589 kB+891 B (+0.2%)
    module381 kB382 kB+563 B (+0.1%)
    react-native382 kB382 kB+562 B (+0.1%)
  • @firebase/firestore-lite

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser111 kB112 kB+584 B (+0.5%)
    main153 kB154 kB+911 B (+0.6%)
    module111 kB112 kB+584 B (+0.5%)
    react-native111 kB112 kB+584 B (+0.5%)
  • @firebase/functions

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser13.7 kB14.0 kB+313 B (+2.3%)
    main14.2 kB14.6 kB+306 B (+2.1%)
    module13.7 kB14.0 kB+313 B (+2.3%)
  • @firebase/storage

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser57.8 kB58.0 kB+128 B (+0.2%)
    main59.3 kB59.4 kB+111 B (+0.2%)
    module57.8 kB58.0 kB+128 B (+0.2%)
  • @firebase/vertexai

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    browser28.8 kB29.0 kB+220 B (+0.8%)
    main29.6 kB29.9 kB+203 B (+0.7%)
    module28.8 kB29.0 kB+220 B (+0.8%)
  • bundle

    46 size changes

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    analytics (logEvent)44.6 kB44.8 kB+173 B (+0.4%)
    app-check (CustomProvider)37.5 kB37.6 kB+173 B (+0.5%)
    app-check (ReCaptchaEnterpriseProvider)40.0 kB40.2 kB+173 B (+0.4%)
    app-check (ReCaptchaV3Provider)39.9 kB40.1 kB+173 B (+0.4%)
    auth (Anonymous)76.2 kB76.6 kB+303 B (+0.4%)
    auth (EmailAndPassword)86.4 kB86.7 kB+303 B (+0.4%)
    auth (GoogleFBTwitterGitHubPopup)103 kB103 kB+303 B (+0.3%)
    auth (GooglePopup)100 kB101 kB+303 B (+0.3%)
    auth (GoogleRedirect)100 kB101 kB+303 B (+0.3%)
    auth (Phone)93.8 kB94.1 kB+303 B (+0.3%)
    database (Append to a list of data)149 kB150 kB+545 B (+0.4%)
    database (Filtering data)148 kB149 kB+545 B (+0.4%)
    database (Listen for child events)164 kB165 kB+545 B (+0.3%)
    database (Listen for value events + Detach listeners)164 kB165 kB+545 B (+0.3%)
    database (Listen for value events)164 kB165 kB+545 B (+0.3%)
    database (Read data once)164 kB164 kB+545 B (+0.3%)
    database (Save data as transactions)166 kB167 kB+545 B (+0.3%)
    database (Sort data)150 kB150 kB+545 B (+0.4%)
    database (Write data)148 kB149 kB+545 B (+0.4%)
    firestore (CSI Auto Indexing Disable and Delete)272 kB273 kB+733 B (+0.3%)
    firestore (CSI Auto Indexing Enable)272 kB273 kB+733 B (+0.3%)
    firestore (Persistence)303 kB304 kB+733 B (+0.2%)
    firestore (Query Cursors)249 kB250 kB+730 B (+0.3%)
    firestore (Query)247 kB247 kB+730 B (+0.3%)
    firestore (Read data once)234 kB235 kB+730 B (+0.3%)
    firestore (Read Write w Persistence)328 kB329 kB+733 B (+0.2%)
    firestore (Realtime updates)237 kB237 kB+730 B (+0.3%)
    firestore (Transaction)214 kB214 kB+730 B (+0.3%)
    firestore (Write data)213 kB214 kB+730 B (+0.3%)
    firestore-lite (Query Cursors)91.4 kB103 kB+11.7 kB (+12.8%)
    firestore-lite (Query)87.6 kB99.2 kB+11.7 kB (+13.3%)
    firestore-lite (Read data once)63.0 kB74.7 kB+11.7 kB (+18.5%)
    firestore-lite (Transaction)88.3 kB99.9 kB+11.7 kB (+13.2%)
    firestore-lite (Write data)72.6 kB84.3 kB+11.7 kB (+16.0%)
    functions (call)34.4 kB34.9 kB+491 B (+1.4%)
    messaging (send + receive)47.4 kB47.5 kB+173 B (+0.4%)
    performance (trace)51.8 kB52.0 kB+173 B (+0.3%)
    remote-config (getAndFetch)46.3 kB46.5 kB+173 B (+0.4%)
    storage (getBytes)42.1 kB42.5 kB+348 B (+0.8%)
    storage (getDownloadURL)44.2 kB44.5 kB+348 B (+0.8%)
    storage (getMetadata)43.6 kB44.0 kB+348 B (+0.8%)
    storage (list + listAll)43.1 kB43.4 kB+348 B (+0.8%)
    storage (updateMetadata)43.9 kB44.3 kB+348 B (+0.8%)
    storage (uploadBytes)48.8 kB49.1 kB+348 B (+0.7%)
    storage (uploadBytesResumable)58.7 kB59.1 kB+348 B (+0.6%)
    storage (uploadString)49.0 kB49.3 kB+348 B (+0.7%)

  • firebase

    19 size changes

    TypeBase (46c91bc)Merge (eba1cf8)Diff
    firebase-app-compat.js31.8 kB32.5 kB+668 B (+2.1%)
    firebase-app.js101 kB103 kB+1.79 kB (+1.8%)
    firebase-auth-compat.js143 kB143 kB+109 B (+0.1%)
    firebase-auth-cordova.js136 kB136 kB+87 B (+0.1%)
    firebase-auth-web-extension.js119 kB119 kB+87 B (+0.1%)
    firebase-auth.js155 kB155 kB+87 B (+0.1%)
    firebase-compat.js797 kB799 kB+1.81 kB (+0.2%)
    firebase-data-connect.js16.7 kB16.9 kB+170 B (+1.0%)
    firebase-database-compat.js166 kB166 kB+305 B (+0.2%)
    firebase-database.js186 kB187 kB+309 B (+0.2%)
    firebase-firestore-compat.js346 kB347 kB+491 B (+0.1%)
    firebase-firestore-lite.js119 kB130 kB+11.4 kB (+9.6%)
    firebase-firestore.js440 kB441 kB+540 B (+0.1%)
    firebase-functions-compat.js10.4 kB10.6 kB+231 B (+2.2%)
    firebase-functions.js14.6 kB14.8 kB+236 B (+1.6%)
    firebase-performance-standalone-compat.js93.7 kB94.3 kB+671 B (+0.7%)
    firebase-storage-compat.js40.3 kB40.4 kB+109 B (+0.3%)
    firebase-storage.js46.2 kB46.3 kB+113 B (+0.2%)
    firebase-vertexai.js23.8 kB24.0 kB+177 B (+0.7%)

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/nrMV7vbGkV.html

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Nov 22, 2024

Size Analysis Report 1

This report is too large (446,176 characters) to be displayed here in a GitHub comment. Please use the below link to see the full report on Google Cloud Storage.

Test Logs

  1. https://storage.googleapis.com/firebase-sdk-metric-reports/xKgiuvnWDP.html

Copy link

changeset-bot bot commented Dec 4, 2024

🦋 Changeset detected

Latest commit: d6e1917

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@firebase/app Minor
firebase Minor
@firebase/data-connect Patch
@firebase/firestore Patch
@firebase/functions Patch
@firebase/database Patch
@firebase/vertexai Patch
@firebase/storage Patch
@firebase/auth Patch
@firebase/app-compat Patch
@firebase/firestore-compat Patch
@firebase/functions-compat Patch
@firebase/database-compat Patch
@firebase/storage-compat Patch
@firebase/auth-compat Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

constructor(
private appName_: string,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This value was never used.

private appCheckProvider?: Provider<AppCheckInternalComponentName>
) {
if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
Copy link
Contributor Author

@DellaBitta DellaBitta Dec 16, 2024

Choose a reason for hiding this comment

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

During the initialization of the Data Connect-specific AppCheckTokenProvider, check if the provided app is a FirebaseServerApp that contains an App Check token. If it does, store the token locally so getToken can return it (below).

this.appName = app.name;
if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
Copy link
Contributor Author

@DellaBitta DellaBitta Dec 16, 2024

Choose a reason for hiding this comment

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

Like Data Connect above, check if the provided app is a FirebaseServerApp that contains an App Check token. If it does, store the token locally so getToken can return it (below).

if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
}
Copy link
Contributor Author

@DellaBitta DellaBitta Dec 16, 2024

Choose a reason for hiding this comment

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

Like the other SDKs (above), for Firestore check if the provided app is a FirebaseServerApp that contains an App Check token. If it does, store the token locally so getToken can return it (below).

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any concern of this token expiring?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I attempted to answer this in the main thread of the PR.

authProvider: Provider<FirebaseAuthInternalName>,
messagingProvider: Provider<MessagingInternalComponentName>,
appCheckProvider: Provider<AppCheckInternalComponentName>
) {
if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
Copy link
Contributor Author

@DellaBitta DellaBitta Dec 16, 2024

Choose a reason for hiding this comment

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

Like the other SDKs (above), for Functions check if the provided app is a FirebaseServerApp that contains an App Check token. If it does, store the token locally so getToken can return it (below).

@@ -262,6 +266,9 @@ export class FirebaseStorageImpl implements FirebaseStorage {
}

async _getAppCheckToken(): Promise<string | null> {
if (_isFirebaseServerApp(this.app) && this.app.settings.appCheckToken) {
return this.app.settings.appCheckToken;
}
Copy link
Contributor Author

@DellaBitta DellaBitta Dec 16, 2024

Choose a reason for hiding this comment

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

Check to see if Storage's app is a FirebaseServerApp that contains an App Check token. If it does, return the token instead of invoking App Check.

this._apiSettings.getAppCheckToken = () => {
return Promise.resolve({ token });
};
} else if ((vertexAI as VertexAIService).appCheck) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similiar to the other SDKs (above) check to see if the provided instance of FirebaseApp is one of FirebaseServerApp, and if it contains an App Check token, then return it instead of invoking the App Check SDK.

This implementation differes only slightly to the other SDKs (which use Providers) becuase we already have a direct reference to the FirebaseApp instance itself, similiar to the Storage implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this PR is the time to do it, but we should probably abstract all this logic (maybe also the model name processing) out of GenerativeModel to some kind of shared function that ImagenModel can also use, will give @dlarocque a heads up.


await deleteApp(serverApp);
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Invalid tokens are now handled by a new flow, which we test in app/src/FirebaseServerApp.test.ts, so I'm removing this test here.

@DellaBitta DellaBitta marked this pull request as ready for review December 17, 2024 18:11
@DellaBitta DellaBitta requested review from a team as code owners December 17, 2024 18:11
Copy link
Contributor

@MarkDuckworth MarkDuckworth left a comment

Choose a reason for hiding this comment

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

Looks okay. One comment. Also, we we're wondering if/how this solution deals with the expiration or short lifespan of app check tokens?

this.appName = app.name;
if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
this.appCheck = appCheckProvider?.getImmediate({ optional: true });
if (!this.appCheck) {
appCheckProvider?.get().then(appCheck => (this.appCheck = appCheck));
}
}

getToken(forceRefresh?: boolean): Promise<AppCheckTokenResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

if forceRefresh && this.serverAppAppCheckToken should this throw or otherwise indicate that an unsupported call was made?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just checking: is the only time that forceRefresh is set to true for the App Check token is when the service returns an UNAUTHENTICATED error?

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like, in RTDB, it will be set to true on any failed request (status !== 'ok') to the auth or app check endpoints.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose that throwing here would then be valid, as it would reduce the burden on the service on subsequent requests, if the app attempts to continue to use the defunct App Check Token. WDYT @hsubox76 ?

@DellaBitta
Copy link
Contributor Author

DellaBitta commented Dec 18, 2024

Looks okay. One comment. Also, we we're wondering if/how this solution deals with the expiration or short lifespan of app check tokens?

It's assumed that they will be created via getToken on the client-side and then passed up to the SSR phase via cookies or service workers, probably at first at client sign in.

We'll post a best practice that apps attempt to use that App Check token on the server side, and either check the validity of the token themselves, or call initializeServerApp with a try / catch block to check for token expiration. If the server side encounters an expired token then the app should route to a client-side page that regenerates a new token, and then route back to the page that uses SSR.

Copy link
Contributor

@hsubox76 hsubox76 left a comment

Choose a reason for hiding this comment

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

LG, just nits!

this.appName = app.name;
if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
this.appCheck = appCheckProvider?.getImmediate({ optional: true });
if (!this.appCheck) {
appCheckProvider?.get().then(appCheck => (this.appCheck = appCheck));
}
}

getToken(forceRefresh?: boolean): Promise<AppCheckTokenResult> {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like, in RTDB, it will be set to true on any failed request (status !== 'ok') to the auth or app check endpoints.

authProvider: Provider<FirebaseAuthInternalName>,
messagingProvider: Provider<MessagingInternalComponentName>,
appCheckProvider: Provider<AppCheckInternalComponentName>
) {
if (_isFirebaseServerApp(app) && app.settings.appCheckToken) {
this.serverAppAppCheckToken = app.settings.appCheckToken;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't attach comment to line 85 but can we put a ? there so it's appCheckProvider?.get() like RTDB and DataConnect etc seem to have done and I just noticed in this PR, for max efficiency in not bothering to even call get() in the case of no provider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

if ((vertexAI as VertexAIService).appCheck) {

if (
vertexAI.app &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Can there be a vertexAI instance that doesn't have an app property? How does that happen? Actually come to think of it, what do you think about putting a ? here https://github.com/firebase/firebase-js-sdk/blob/main/packages/app/src/internal.ts#L173 so that it can handle being passed undefined/null, so it would cover if any other caller happens to pass it that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To answer your first question, I'm not sure. I don't think it's possible but the rest of the code (above) checks for it.

As for the latter, I'll add the ability for the function to take null / undefined!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

this._apiSettings.getAppCheckToken = () => {
return Promise.resolve({ token });
};
} else if ((vertexAI as VertexAIService).appCheck) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this PR is the time to do it, but we should probably abstract all this logic (maybe also the model name processing) out of GenerativeModel to some kind of shared function that ImagenModel can also use, will give @dlarocque a heads up.

@NhienLam NhienLam self-requested a review December 18, 2024 18:20
Copy link
Contributor

@NhienLam NhienLam left a comment

Choose a reason for hiding this comment

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

Auth: LGTM

@@ -42,7 +47,11 @@ export class AppCheckTokenProvider {
}
}

getToken(forceRefresh?: boolean): Promise<AppCheckTokenResult> {
getToken(): Promise<AppCheckTokenResult> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

forceRefresh was never used, so I've removed it for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants