Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4e8c8aa
Add exception handling covered by standard platform ErrorCode
yvonnep165 Aug 20, 2026
7f56cb3
Add DecodedAppCheckToken, VerifyAppCheckTokenResponse and VerifyAppCh…
yvonnep165 Aug 20, 2026
4341926
Add internal appchecktokenverifier class to support token verificatio…
yvonnep165 Aug 21, 2026
7e4dd47
create the entry point for the Firebase App Check service and fix som…
yvonnep165 Aug 21, 2026
8b672ad
Add unit tests for internal appcheck processing
yvonnep165 Aug 24, 2026
b015d15
add unit tests for decoded app check token
yvonnep165 Aug 24, 2026
c2818a9
add unit tests for firebaseappcheck class
yvonnep165 Aug 24, 2026
2cdabc5
add unit tests for appchecktokenoptions
yvonnep165 Aug 24, 2026
03e1be1
add unit tests for appchecktokenresponse
yvonnep165 Aug 24, 2026
e5269d6
add defensively check for null for audience claim and wrap the httpre…
yvonnep165 Aug 24, 2026
3ebfa55
add 6-hour caching and default refresh timeout to createKeySource
yvonnep165 Aug 25, 2026
208dbe8
initialize the jwtProcessor inside the constructor and remove getJwtP…
yvonnep165 Aug 25, 2026
eebd884
add comment for future issuer check upgrade
yvonnep165 Aug 25, 2026
a737d2b
reverse error messages for badjoseexception and joseexception
yvonnep165 Aug 25, 2026
2c05450
use java.time.Instant for get issued at time and expiration time
yvonnep165 Aug 25, 2026
c30d590
add appid, provider and jti properties exposure
yvonnep165 Aug 25, 2026
8ee2a99
add check for null for setConsume
yvonnep165 Aug 25, 2026
6d444c1
remove getappid from decodedappchecktoken
yvonnep165 Aug 26, 2026
3e0976e
remove the null case in docstring for getIssuedAt and getExpirationTime
yvonnep165 Aug 26, 2026
e28273b
add tests with real RSA cryptography
yvonnep165 Aug 26, 2026
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
120 changes: 120 additions & 0 deletions src/main/java/com/google/firebase/appcheck/DecodedAppCheckToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.appcheck;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
* Represents a verified Firebase App Check token.
*/
public class DecodedAppCheckToken {

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.

If this class is part of the public API, please also consider exposing provider and the optional claim jti.

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.

Added getJti() and getProvider().


private final Map<String, Object> claims;

/**
* Creates an instance of {@link DecodedAppCheckToken} from a map of JWT claims.
*
* @param claims A map of JWT claims.
*/
public DecodedAppCheckToken(Map<String, Object> claims) {
checkNotNull(claims, "Claims map must not be null");
checkArgument(claims.containsKey("sub"), "Claims map must contain sub");

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.

We should probably also check the other required claims iss, aud, exp, and iat.

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.

This followed the convention from FirebaseToken (auth) and FirebasePhoneNumberVerificationToken, where only sub is strictly required in the constructor and the full claims validation (iss, aud, exp, iat) is handled by AppCheckTokenVerifier before the object is instantiated. Try to keep DecodedAppCheckToken as a flexible claims wrapper allows the getters to provide fallbacks (e.g. getAudience() returning []) and make creating mock tokens in tests much simpler. But let me know if you'd still prefer enforcing all of them here though!

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.

I don't feel too strongly about this topic. Note that for these required claim names, the App Check backend is quite disciplined and will never return empty or null for them. For testing, it's reasonable to have an additional test-only constructor where these checks are not performed.

this.claims = ImmutableMap.copyOf(claims);
}

/**
* Returns the issuer identifier for the token.
*/
public String getIssuer() {
return (String) claims.get("iss");
}

/**
* Returns the subject claim ('sub') of the token.
*/
public String getSubject() {
return (String) claims.get("sub");
}

/**
* Returns the JWT ID ('jti') of the token, or {@code null} if not present.
*/
public String getJti() {
return (String) claims.get("jti");
}

/**
* Returns the attestation provider for this token, or {@code null} if not present.
*/
public String getProvider() {
return (String) claims.get("provider");
}

/**
* Returns the audience for which this token is intended.
*/
public List<String> getAudience() {
Object audience = claims.get("aud");
if (audience instanceof String) {
return ImmutableList.of((String) audience);
} else if (audience instanceof List) {
@SuppressWarnings("unchecked")
List<String> audienceList = (List<String>) audience;
return ImmutableList.copyOf(audienceList);
}
return ImmutableList.of();
}

/**
* Returns the expiration time as an {@link Instant}.
*/
public Instant getExpirationTime() {
return toInstant(claims.get("exp"));
}

/**
* Returns the issued-at time as an {@link Instant}.
*/
public Instant getIssuedAt() {
return toInstant(claims.get("iat"));
}

/**
* Returns the entire map of claims.
*/
public Map<String, Object> getClaims() {
return claims;
}

private static Instant toInstant(Object timeObj) {
if (timeObj instanceof Date) {
return ((Date) timeObj).toInstant();
}
if (timeObj instanceof Number) {
return Instant.ofEpochSecond(((Number) timeObj).longValue());
}
return null;
}
}
139 changes: 139 additions & 0 deletions src/main/java/com/google/firebase/appcheck/FirebaseAppCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.appcheck;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.api.core.ApiFuture;
import com.google.common.annotations.VisibleForTesting;
import com.google.firebase.FirebaseApp;
import com.google.firebase.ImplFirebaseTrampolines;
import com.google.firebase.appcheck.internal.AppCheckTokenVerifier;
import com.google.firebase.internal.CallableOperation;
import com.google.firebase.internal.FirebaseService;

/**
* This class is the entry point for the Firebase App Check service.
*
* <p>You can get an instance of {@link FirebaseAppCheck} via {@link #getInstance()}
* or {@link #getInstance(FirebaseApp)}.
*/
public final class FirebaseAppCheck {

private static final String SERVICE_ID = FirebaseAppCheck.class.getName();

private final FirebaseApp app;
private final AppCheckTokenVerifier tokenVerifier;

private FirebaseAppCheck(FirebaseApp app) {
this(app, new AppCheckTokenVerifier(app));
}

@VisibleForTesting
FirebaseAppCheck(FirebaseApp app, AppCheckTokenVerifier tokenVerifier) {
this.app = checkNotNull(app, "FirebaseApp must not be null");
this.tokenVerifier = checkNotNull(tokenVerifier, "AppCheckTokenVerifier must not be null");
}

/**
* Gets the {@link FirebaseAppCheck} instance for the default {@link FirebaseApp}.
*
* @return The {@link FirebaseAppCheck} instance for the default {@link FirebaseApp}.
*/
public static FirebaseAppCheck getInstance() {
return getInstance(FirebaseApp.getInstance());
}

/**
* Gets the {@link FirebaseAppCheck} instance for the specified {@link FirebaseApp}.
*
* @param app The {@link FirebaseApp} instance.
* @return The {@link FirebaseAppCheck} instance for the specified {@link FirebaseApp}.
*/
public static synchronized FirebaseAppCheck getInstance(FirebaseApp app) {
FirebaseAppCheckService service =
ImplFirebaseTrampolines.getService(app, SERVICE_ID, FirebaseAppCheckService.class);
if (service == null) {
service = ImplFirebaseTrampolines.addService(app, new FirebaseAppCheckService(app));
}
return service.getInstance();
}

/**
* Verifies an App Check token string.
*
* @param appCheckToken The App Check token string to verify.
* @return A {@link VerifyAppCheckTokenResponse} containing the decoded token.
* @throws FirebaseAppCheckException If verification fails.
*/
public VerifyAppCheckTokenResponse verifyToken(String appCheckToken)
throws FirebaseAppCheckException {
return verifyToken(appCheckToken, null);
}

/**
* Verifies an App Check token string with options.
*
* @param appCheckToken The App Check token string to verify.
* @param options Verification options specified via {@link VerifyAppCheckTokenOptions}.
* @return A {@link VerifyAppCheckTokenResponse} containing the decoded token
* and consumption status.
* @throws FirebaseAppCheckException If verification fails.
*/
public VerifyAppCheckTokenResponse verifyToken(
String appCheckToken, VerifyAppCheckTokenOptions options) throws FirebaseAppCheckException {
return this.tokenVerifier.verifyToken(appCheckToken, options);
}

/**
* Asynchronously verifies an App Check token string.
*
* @param appCheckToken The App Check token string to verify.
* @return An {@link ApiFuture} containing the {@link VerifyAppCheckTokenResponse}.
*/
public ApiFuture<VerifyAppCheckTokenResponse> verifyTokenAsync(String appCheckToken) {
return verifyTokenAsync(appCheckToken, null);
}

/**
* Asynchronously verifies an App Check token string with options.
*
* @param appCheckToken The App Check token string to verify.
* @param options Verification options specified via {@link VerifyAppCheckTokenOptions}.
* @return An {@link ApiFuture} containing the {@link VerifyAppCheckTokenResponse}.
*/
public ApiFuture<VerifyAppCheckTokenResponse> verifyTokenAsync(
String appCheckToken, VerifyAppCheckTokenOptions options) {
return verifyTokenOp(appCheckToken, options).callAsync(this.app);
}

private CallableOperation<VerifyAppCheckTokenResponse, FirebaseAppCheckException> verifyTokenOp(
final String appCheckToken, final VerifyAppCheckTokenOptions options) {
return new CallableOperation<VerifyAppCheckTokenResponse, FirebaseAppCheckException>() {
@Override
protected VerifyAppCheckTokenResponse execute() throws FirebaseAppCheckException {
return verifyToken(appCheckToken, options);
}
};
}

private static class FirebaseAppCheckService extends FirebaseService<FirebaseAppCheck> {
FirebaseAppCheckService(FirebaseApp app) {
super(SERVICE_ID, new FirebaseAppCheck(app));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.appcheck;

import com.google.firebase.ErrorCode;
import com.google.firebase.FirebaseException;
import com.google.firebase.IncomingHttpResponse;
import com.google.firebase.internal.NonNull;
import com.google.firebase.internal.Nullable;

/**
* Generic exception related to Firebase App Check. Check the error code and message for more
* details.
*/
public class FirebaseAppCheckException extends FirebaseException {

public FirebaseAppCheckException(
@NonNull ErrorCode errorCode,
@NonNull String message,
@Nullable Throwable cause,
@Nullable IncomingHttpResponse response) {
super(errorCode, message, cause, response);
}

public FirebaseAppCheckException(
@NonNull ErrorCode errorCode,
@NonNull String message,
@Nullable Throwable cause) {
this(errorCode, message, cause, null);
}

public FirebaseAppCheckException(
@NonNull ErrorCode errorCode,
@NonNull String message) {
this(errorCode, message, null, null);
}

public FirebaseAppCheckException(@NonNull FirebaseException base) {
this(base.getErrorCode(), base.getMessage(), base.getCause(), base.getHttpResponse());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.appcheck;

import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Optional;

/**
* Options for verifying a Firebase App Check token.
*/
public final class VerifyAppCheckTokenOptions {

private final Optional<Boolean> consume;

private VerifyAppCheckTokenOptions(Builder builder) {
this.consume = builder.consume;
}

/**
* Returns whether to consume the App Check token during verification for replay protection.
*/
public Optional<Boolean> getConsume() {
return consume;
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {

private Optional<Boolean> consume = Optional.empty();

private Builder() {}

/**
* Sets whether to consume the token during verification.
*
* @param consume Set to true to consume the token.
* @return This builder.
*/
public Builder setConsume(boolean consume) {
this.consume = Optional.of(consume);
return this;
}

/**
* Sets whether to consume the token during verification.
*
* @param consume Optional boolean value. Must not be null.
* @return This builder.
*/
public Builder setConsume(Optional<Boolean> consume) {
this.consume = checkNotNull(consume, "consume must not be null");
return this;
}

/**
* Builds a new {@link VerifyAppCheckTokenOptions} instance.
*/
public VerifyAppCheckTokenOptions build() {
return new VerifyAppCheckTokenOptions(this);
}
}
}
Loading
Loading