-
Notifications
You must be signed in to change notification settings - Fork 306
feat(appcheck): Add App Check token verification support with replay protection #1233
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
base: main
Are you sure you want to change the base?
Changes from all commits
4e8c8aa
7f56cb3
4341926
7e4dd47
8b672ad
b015d15
c2818a9
2cdabc5
03e1be1
e5269d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| /* | ||
| * 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.util.Date; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Represents a verified Firebase App Check token. | ||
| */ | ||
| public class DecodedAppCheckToken { | ||
|
|
||
| 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"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably also check the other required claims |
||
| 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 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 in seconds since the Unix epoch. | ||
| */ | ||
| public long getExpirationTime() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
| Object exp = claims.get("exp"); | ||
| if (exp instanceof Date) { | ||
| return ((Date) exp).getTime() / 1000L; | ||
| } | ||
| return exp instanceof Number ? ((Number) exp).longValue() : 0L; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the issued-at time in seconds since the Unix epoch. | ||
| */ | ||
| public long getIssuedAt() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
| Object iat = claims.get("iat"); | ||
| if (iat instanceof Date) { | ||
| return ((Date) iat).getTime() / 1000L; | ||
| } | ||
| return iat instanceof Number ? ((Number) iat).longValue() : 0L; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the entire map of claims. | ||
| */ | ||
| public Map<String, Object> getClaims() { | ||
| return claims; | ||
| } | ||
| } | ||
| 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,78 @@ | ||
| /* | ||
| * 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 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. | ||
| * @return This builder. | ||
| */ | ||
| public Builder setConsume(Optional<Boolean> consume) { | ||
| this.consume = consume != null ? consume : Optional.<Boolean>empty(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion, allowing |
||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Builds a new {@link VerifyAppCheckTokenOptions} instance. | ||
| */ | ||
| public VerifyAppCheckTokenOptions build() { | ||
| return new VerifyAppCheckTokenOptions(this); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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
providerand the optional claimjti.