Skip to content

feat(appcheck): Verify one-time tokens for replay protection - #774

Merged
yvonnep165 merged 26 commits into
devfrom
yp-verify-one-time-token
Oct 8, 2026
Merged

yvonnep165 merged 26 commits into
devfrom
yp-verify-one-time-token

Conversation

@yvonnep165

@yvonnep165 yvonnep165 commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

This PR introduces the VerifyOneTimeToken method to the appcheck package, enabling stateful replay protection for Firebase App Check.

  • Implement a new method VerifyOneTimeToken that first performs local, stateless JWT verification followed by a stateful POST request to backend endpoint to consume the token.
  • Add the AlreadyConsumed (*bool) field to the DecodedAppCheckToken struct.
  • Plumb option.ClientOption from the main app initialization down to internal.AppCheckConfig.
  • Add unit tests.

@yvonnep165 yvonnep165 changed the title feat(appcheck): verify one-time tokens for replay protection feat(appcheck): Verify one-time tokens for replay protection Aug 10, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the VerifyOneTimeToken method to the App Check client, enabling stateful verification and consumption of one-time tokens. It also adds the AlreadyConsumed field to DecodedAppCheckToken and integrates HTTP client options. Feedback on the tests suggests using a helper function to instantiate *bool pointers to reduce verbosity, and restoring the mutated global verifyURLFormat variable to prevent potential race conditions and side effects.

Comment thread appcheck/appcheck_test.go
Comment thread appcheck/appcheck_test.go
@yvonnep165 yvonnep165 self-assigned this Aug 10, 2026
@yvonnep165 yvonnep165 added release-note release:stage Stage a release candidate labels Aug 10, 2026

@weixifan weixifan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thank you for the PR; LGTM with just a minor comment.

Comment thread appcheck/appcheck.go
const appCheckIssuer = "https://firebaseappcheck.googleapis.com/"

var (
verifyURLFormat = "https://firebaseappcheck.googleapis.com/v1beta/projects/%s:verifyAppCheckToken"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Our v1 endpoint is almost ready, and that would be our preferred URL when this SDK is released. Maybe we can just change this to v1 now?

I'm also fine with leaving this alone for now and wait until our v1 endpoint is fully published. In that case, is a TODO appropriate here?

@yvonnep165 yvonnep165 Aug 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the quick review! Sure, we can switch to the v1 endpoint as long as we can verify and run our tests against it. Currently, we only have unit tests in place so we can just change to v1 now, but we plan to add integration tests against the live endpoint once we implement the token creation methods.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One thing we should keep in mind if we do merge this as v1, is that our next release will include this whether or not the backend is ready. Lets set v1 but hold back merging until the backend is live.

@jonathanedey jonathanedey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM with one note on v1 endpoint. Thanks!

@yvonnep165
yvonnep165 requested a review from a team September 23, 2026 15:07

@lahirumaramba lahirumaramba left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thank you!

Comment thread appcheck/appcheck.go
if err != nil {
return nil, err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit because I am unsure if we track this.
This is missing the usage headers

	hc.Opts = []internal.HTTPOption{
		internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(config.Version)),
	}

Comment thread appcheck/appcheck.go Outdated
//
// This method performs the same stateless verification as VerifyToken. In addition, it makes a
// stateful network call to the Firebase App Check backend to ensure that the token has not been
// consumed previously. If the token is valid, it is marked as consumed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we can add a few things in here. It would be nice to make it clear that this method doesn't throw an error for already consumed tokens. Maybe we can incorporate some wordings from the Node.js docs?

/**
   * To use the replay protection feature, set this to `true`. The {@link AppCheck.verifyToken}
   * method will mark the token as consumed after verifying it.
   * 
   * Tokens that are found to be already consumed will be marked as such in the response.
   * 
   * Tokens are only considered to be consumed if it is sent to App Check backend by calling the
   * {@link AppCheck.verifyToken} method with this field set to `true`; other uses of the token
   * do not consume it.
   * 
   * This replay protection feature requires an additional network call to the App Check backend
   * and forces your clients to obtain a fresh attestation from your chosen attestation providers.
   * This can therefore negatively impact performance and can potentially deplete your attestation
   * providers' quotas faster. We recommend that you use this feature only for protecting
   * low volume, security critical, or expensive operations.
   */

Comment thread appcheck/appcheck.go
jwks, err := keyfunc.Get(JWKSUrl, keyfunc.Options{
Ctx: ctx,
RefreshInterval: 6 * time.Hour,
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we add a check for ProjectID here?

    if c.ProjectID == "" {
		return nil, errors.New("project ID is required to access Firebase App Check client")
	}

@yvonnep165

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for verifying and consuming one-time App Check tokens by introducing the VerifyOneTimeToken method, which enables replay protection. It also updates the JWKS URL to the v1 endpoint, adds project ID validation in NewClient, and configures an internal HTTP client with metrics headers. The review feedback suggests adding a nil check for the configuration parameter in NewClient to prevent potential nil pointer dereferences, and appending to hc.Opts instead of overwriting it to preserve existing client options.

Comment thread appcheck/appcheck.go
Comment on lines +83 to +85
if conf.ProjectID == "" {
return nil, errors.New("project ID is required to access Firebase App Check client")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To prevent a potential nil pointer dereference, verify that conf is not nil before accessing its fields.

	if conf == nil {
		return nil, errors.New("appcheck config is required")
	}
	if conf.ProjectID == "" {
		return nil, errors.New("project ID is required to access Firebase App Check client")
	}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

NewClient is only called internally from App.AppCheck, which always passes a non-nil config, and internal.AppCheckConfig can't be constructed outside the module. This also matches the other service constructors, which don't nil-check their config either, so I'd prefer to keep it consistent.

Comment thread appcheck/appcheck.go
Comment on lines +98 to +100
hc.Opts = []internal.HTTPOption{
internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(conf.Version)),
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Instead of overwriting hc.Opts, append to it. This ensures that any existing options configured on the client are preserved.

	hc.Opts = append(hc.Opts, internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(conf.Version)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hc is freshly created by internal.NewHTTPClient on the line above, which never populates Opts, so there's nothing to preserve and both forms behave identically.

@yvonnep165
yvonnep165 merged commit c41aefe into dev Oct 8, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:stage Stage a release candidate release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants