Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
103 changes: 103 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,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 {

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.


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.

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() {

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 think java.time.Instant is a better candidate to hold instants in time, since we have access to Java 8.

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() {

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.

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;
}
}
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,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();

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.

In my opinion, allowing nulls into an optional parameter re-exposes the exact same problem that Optional<> was intended to solve. I think it's better for us to check for nullness and throw in this case.

return this;
}

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