Skip to content

Commit efee9bb

Browse files
committed
Add initial classes for App Check
1 parent c213726 commit efee9bb

9 files changed

+606
-0
lines changed

pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@
455455
<artifactId>netty-transport</artifactId>
456456
<version>${netty.version}</version>
457457
</dependency>
458+
<dependency>
459+
<groupId>com.nimbusds</groupId>
460+
<artifactId>nimbus-jose-jwt</artifactId>
461+
<version>9.22</version>
462+
</dependency>
458463

459464
<!-- Test Dependencies -->
460465
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.appcheck;
18+
19+
/**
20+
* Error codes that can be raised by the App Check APIs.
21+
*/
22+
public enum AppCheckErrorCode {
23+
24+
/**
25+
* One or more arguments specified in the request were invalid.
26+
*/
27+
INVALID_ARGUMENT,
28+
29+
/**
30+
* Internal server error.
31+
*/
32+
INTERNAL,
33+
34+
/**
35+
* User is not authenticated.
36+
*/
37+
UNAUTHENTICATED,
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2022 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.appcheck;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.common.base.Strings;
22+
import com.google.firebase.ErrorCode;
23+
import com.nimbusds.jose.JOSEException;
24+
import com.nimbusds.jose.JWSAlgorithm;
25+
import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
26+
import com.nimbusds.jose.jwk.source.JWKSetCache;
27+
import com.nimbusds.jose.jwk.source.JWKSource;
28+
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
29+
import com.nimbusds.jose.proc.BadJOSEException;
30+
import com.nimbusds.jose.proc.JWSKeySelector;
31+
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
32+
import com.nimbusds.jose.proc.SecurityContext;
33+
import com.nimbusds.jwt.JWTClaimsSet;
34+
import com.nimbusds.jwt.SignedJWT;
35+
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
36+
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
37+
38+
import java.net.MalformedURLException;
39+
import java.net.URL;
40+
import java.text.ParseException;
41+
import java.util.concurrent.TimeUnit;
42+
43+
final class AppCheckTokenVerifier {
44+
45+
private final URL jwksUrl;
46+
private final String projectId;
47+
48+
private static final String JWKS_URL = "https://firebaseappcheck.googleapis.com/v1/jwks";
49+
private static final String APP_CHECK_ISSUER = "https://firebaseappcheck.googleapis.com/";
50+
51+
private AppCheckTokenVerifier(Builder builder) {
52+
checkArgument(!Strings.isNullOrEmpty(builder.projectId));
53+
this.projectId = builder.projectId;
54+
try {
55+
this.jwksUrl = new URL(JWKS_URL);
56+
} catch (MalformedURLException e) {
57+
throw new IllegalArgumentException("Malformed JWK url string", e);
58+
}
59+
}
60+
61+
/**
62+
* Verifies that the given App Check token string is a valid Firebase JWT.
63+
*
64+
* @param token The token string to be verified.
65+
* @return A decoded representation of the input token string.
66+
* @throws FirebaseAppCheckException If the input token string fails to verify due to any reason.
67+
*/
68+
DecodedAppCheckToken verifyToken(String token) throws FirebaseAppCheckException {
69+
SignedJWT signedJWT;
70+
JWTClaimsSet claimsSet;
71+
String scopedProjectId = String.format("projects/%s", projectId);
72+
String projectIdMatchMessage = " Make sure the App Check token comes from the same "
73+
+ "Firebase project as the service account used to authenticate this SDK.";
74+
75+
try {
76+
signedJWT = SignedJWT.parse(token);
77+
claimsSet = signedJWT.getJWTClaimsSet();
78+
} catch (java.text.ParseException e) {
79+
// Invalid signed JWT encoding
80+
throw new FirebaseAppCheckException(ErrorCode.INVALID_ARGUMENT, "Invalid token");
81+
}
82+
83+
String errorMessage = null;
84+
85+
if (!signedJWT.getHeader().getAlgorithm().equals(JWSAlgorithm.RS256)) {
86+
errorMessage = String.format("The provided App Check token has incorrect algorithm. "
87+
+ "Expected 'RS256' but got '%s'.", signedJWT.getHeader().getAlgorithm());
88+
} else if (!signedJWT.getHeader().getType().getType().equals("JWT")) {
89+
errorMessage = String.format("The provided App Check token has invalid type header."
90+
+ "Expected %s but got %s", "JWT", signedJWT.getHeader().getType().getType());
91+
} else if (!claimsSet.getAudience().contains(scopedProjectId)) {
92+
errorMessage = String.format("The provided App Check token has incorrect 'aud' (audience) "
93+
+ "claim. Expected %s but got %s. %s",
94+
scopedProjectId, claimsSet.getAudience().toString(), projectIdMatchMessage);
95+
} else if (!claimsSet.getIssuer().startsWith(APP_CHECK_ISSUER)) {
96+
errorMessage = "invalid iss";
97+
} else if (claimsSet.getSubject().isEmpty()) {
98+
errorMessage = "invalid sub";
99+
}
100+
101+
if (errorMessage != null) {
102+
throw new FirebaseAppCheckException(ErrorCode.INVALID_ARGUMENT, errorMessage);
103+
}
104+
105+
// Create a JWT processor for the access tokens
106+
ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
107+
108+
// Cache the keys for 6 hours
109+
JWKSetCache jwkSetCache = new DefaultJWKSetCache(6L, 6L, TimeUnit.HOURS);
110+
JWKSource<SecurityContext> keySource = new RemoteJWKSet<>(this.jwksUrl, null, jwkSetCache);
111+
112+
// Configure the JWT processor with a key selector to feed matching public
113+
// RSA keys sourced from the JWK set URL.
114+
JWSKeySelector<SecurityContext> keySelector =
115+
new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keySource);
116+
117+
jwtProcessor.setJWSKeySelector(keySelector);
118+
119+
try {
120+
claimsSet = jwtProcessor.process(token, null);
121+
System.out.println(claimsSet.toJSONObject());
122+
} catch (ParseException | BadJOSEException | JOSEException e) {
123+
throw new FirebaseAppCheckException(ErrorCode.INVALID_ARGUMENT, e.getMessage());
124+
}
125+
126+
return new DecodedAppCheckToken(claimsSet.getClaims());
127+
}
128+
129+
static AppCheckTokenVerifier.Builder builder() {
130+
return new AppCheckTokenVerifier.Builder();
131+
}
132+
133+
static final class Builder {
134+
135+
private String projectId;
136+
137+
private Builder() {
138+
}
139+
140+
AppCheckTokenVerifier.Builder setProjectId(String projectId) {
141+
this.projectId = projectId;
142+
return this;
143+
}
144+
145+
AppCheckTokenVerifier build() {
146+
return new AppCheckTokenVerifier(this);
147+
}
148+
}
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.appcheck;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.common.collect.ImmutableMap;
22+
23+
import java.util.Map;
24+
25+
/**
26+
* A decoded and verified Firebase App Check token. See {@link FirebaseAppCheck#verifyToken(String)}
27+
* for details on how to obtain an instance of this class.
28+
*/
29+
public final class DecodedAppCheckToken {
30+
31+
private final Map<String, Object> claims;
32+
33+
DecodedAppCheckToken(Map<String, Object> claims) {
34+
checkArgument(claims != null && claims.containsKey("sub"),
35+
"Claims map must at least contain sub");
36+
this.claims = ImmutableMap.copyOf(claims);
37+
}
38+
39+
/** Returns the Subject for this token. */
40+
public String getSubject() {
41+
return (String) claims.get("sub");
42+
}
43+
44+
/** Returns the Issuer for this token. */
45+
public String getIssuer() {
46+
return (String) claims.get("iss");
47+
}
48+
49+
/** Returns the Audience for this token. */
50+
public String getAudience() {
51+
return (String) claims.get("aud");
52+
}
53+
54+
/** Returns the Expiration Time for this token. */
55+
public String getExpirationTime() {
56+
return (String) claims.get("exp");
57+
}
58+
59+
/** Returns the Issued At for this token. */
60+
public String getIssuedAt() {
61+
return (String) claims.get("iat");
62+
}
63+
64+
/** Returns a map of all the claims on this token. */
65+
public Map<String, Object> getClaims() {
66+
return this.claims;
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.appcheck;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import com.google.api.core.ApiFuture;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import com.google.firebase.FirebaseApp;
24+
import com.google.firebase.ImplFirebaseTrampolines;
25+
import com.google.firebase.internal.CallableOperation;
26+
import com.google.firebase.internal.FirebaseService;
27+
import com.google.firebase.internal.NonNull;
28+
29+
/**
30+
* This class is the entry point for all server-side Firebase App Check actions.
31+
*
32+
* <p>You can get an instance of {@link FirebaseAppCheck} via {@link #getInstance(FirebaseApp)},
33+
* and then use it to access App Check services.
34+
*/
35+
public final class FirebaseAppCheck {
36+
37+
private static final String SERVICE_ID = FirebaseAppCheck.class.getName();
38+
private final FirebaseApp app;
39+
private final FirebaseAppCheckClient appCheckClient;
40+
41+
@VisibleForTesting
42+
FirebaseAppCheck(FirebaseApp app, FirebaseAppCheckClient client) {
43+
this.app = checkNotNull(app);
44+
this.appCheckClient = checkNotNull(client);
45+
}
46+
47+
private FirebaseAppCheck(FirebaseApp app) {
48+
this(app, FirebaseAppCheckClientImpl.fromApp(app));
49+
}
50+
51+
/**
52+
* Gets the {@link FirebaseAppCheck} instance for the default {@link FirebaseApp}.
53+
*
54+
* @return The {@link FirebaseAppCheck} instance for the default {@link FirebaseApp}.
55+
*/
56+
public static FirebaseAppCheck getInstance() {
57+
return getInstance(FirebaseApp.getInstance());
58+
}
59+
60+
/**
61+
* Gets the {@link FirebaseAppCheck} instance for the specified {@link FirebaseApp}.
62+
*
63+
* @return The {@link FirebaseAppCheck} instance for the specified {@link FirebaseApp}.
64+
*/
65+
public static synchronized FirebaseAppCheck getInstance(FirebaseApp app) {
66+
FirebaseAppCheck.FirebaseAppCheckService service = ImplFirebaseTrampolines.getService(app,
67+
SERVICE_ID,
68+
FirebaseAppCheck.FirebaseAppCheckService.class);
69+
if (service == null) {
70+
service = ImplFirebaseTrampolines.addService(app,
71+
new FirebaseAppCheck.FirebaseAppCheckService(app));
72+
}
73+
return service.getInstance();
74+
}
75+
76+
/**
77+
* Verifies a given App Check Token.
78+
*
79+
* @param token The App Check token to be verified.
80+
* @return A {@link VerifyAppCheckTokenResponse}.
81+
* @throws FirebaseAppCheckException If an error occurs while getting the template.
82+
*/
83+
public VerifyAppCheckTokenResponse verifyToken(
84+
@NonNull String token) throws FirebaseAppCheckException {
85+
return verifyTokenOp(token).call();
86+
}
87+
88+
/**
89+
* Similar to {@link #verifyToken(String token)} but performs the operation
90+
* asynchronously.
91+
*
92+
* @param token The App Check token to be verified.
93+
* @return An {@code ApiFuture} that completes with a {@link VerifyAppCheckTokenResponse} when
94+
* the provided token is valid.
95+
*/
96+
public ApiFuture<VerifyAppCheckTokenResponse> verifyTokenAsync(@NonNull String token)
97+
throws FirebaseAppCheckException {
98+
return verifyTokenOp(token).callAsync(app);
99+
}
100+
101+
private CallableOperation<VerifyAppCheckTokenResponse, FirebaseAppCheckException> verifyTokenOp(
102+
final String token) {
103+
final FirebaseAppCheckClient appCheckClient = getAppCheckClient();
104+
return new CallableOperation<VerifyAppCheckTokenResponse, FirebaseAppCheckException>() {
105+
@Override
106+
protected VerifyAppCheckTokenResponse execute() throws FirebaseAppCheckException {
107+
return appCheckClient.verifyToken(token);
108+
}
109+
};
110+
}
111+
112+
@VisibleForTesting
113+
FirebaseAppCheckClient getAppCheckClient() {
114+
return appCheckClient;
115+
}
116+
117+
private static class FirebaseAppCheckService extends FirebaseService<FirebaseAppCheck> {
118+
FirebaseAppCheckService(FirebaseApp app) {
119+
super(SERVICE_ID, new FirebaseAppCheck(app));
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)