-
Notifications
You must be signed in to change notification settings - Fork 304
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
3ebfa55
208dbe8
eebd884
a737d2b
2c05450
c30d590
8ee2a99
6d444c1
3e0976e
e28273b
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,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 { | ||
|
|
||
| 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
Contributor
Author
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. This followed the convention from
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 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; | ||
| } | ||
| } | ||
| 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); | ||
| } | ||
| } | ||
| } |
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.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.
Added
getJti()andgetProvider().