Skip to content

Commit bc8360f

Browse files
author
pavlo
committed
feature/app-check/test
1 parent 7760b87 commit bc8360f

16 files changed

+525
-432
lines changed
Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,113 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Net.Http;
5+
using System.Reflection.Metadata.Ecma335;
46
using System.Threading.Tasks;
57
using FirebaseAdmin;
6-
using FirebaseAdmin.Auth;
8+
using FirebaseAdmin.Auth.Jwt;
9+
using FirebaseAdmin.Auth.Tests;
710
using FirebaseAdmin.Check;
811
using Google.Apis.Auth.OAuth2;
12+
using Moq;
13+
using Newtonsoft.Json.Linq;
914
using Xunit;
1015

1116
namespace FirebaseAdmin.Tests
1217
{
1318
public class FirebaseAppCheckTests : IDisposable
1419
{
15-
[Fact]
16-
public async Task CreateTokenFromAppId()
20+
private readonly string appId = "1:1234:android:1234";
21+
private FirebaseApp mockCredentialApp;
22+
23+
public FirebaseAppCheckTests()
1724
{
18-
string filePath = @"C:\path\to\your\file.txt";
19-
string fileContent = File.ReadAllText(filePath);
20-
string[] appIds = fileContent.Split(',');
21-
foreach (string appId in appIds)
25+
var credential = GoogleCredential.FromFile("./resources/service_account.json");
26+
var options = new AppOptions()
2227
{
23-
var token = await FirebaseAppCheck.CreateToken(appId);
24-
Assert.IsType<string>(token.Token);
25-
Assert.NotNull(token.Token);
26-
Assert.IsType<int>(token.TtlMillis);
27-
Assert.Equal<int>(3600000, token.TtlMillis);
28-
}
28+
Credential = credential,
29+
};
30+
this.mockCredentialApp = FirebaseApp.Create(options);
2931
}
3032

3133
[Fact]
32-
public async Task CreateTokenFromAppIdAndTtlMillis()
34+
public void CreateAppCheck()
35+
{
36+
FirebaseAppCheck withoutAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
37+
Assert.NotNull(withoutAppIdCreate);
38+
}
39+
40+
[Fact]
41+
public async Task InvalidAppIdCreateToken()
42+
{
43+
FirebaseAppCheck invalidAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
44+
45+
await Assert.ThrowsAsync<ArgumentException>(() => invalidAppIdCreate.CreateToken(appId: null));
46+
await Assert.ThrowsAsync<ArgumentException>(() => invalidAppIdCreate.CreateToken(appId: string.Empty));
47+
}
48+
49+
[Fact]
50+
public void WithoutProjectIDCreate()
3351
{
34-
string filePath = @"C:\path\to\your\file.txt";
35-
string fileContent = File.ReadAllText(filePath);
36-
string[] appIds = fileContent.Split(',');
37-
foreach (string appId in appIds)
52+
// Project ID not set in the environment.
53+
Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", null);
54+
Environment.SetEnvironmentVariable("GCLOUD_PROJECT", null);
55+
56+
var options = new AppOptions()
3857
{
39-
AppCheckTokenOptions options = new (1800000);
40-
var token = await FirebaseAppCheck.CreateToken(appId, options);
41-
Assert.IsType<string>(token.Token);
42-
Assert.NotNull(token.Token);
43-
Assert.IsType<int>(token.TtlMillis);
44-
Assert.Equal<int>(1800000, token.TtlMillis);
45-
}
58+
Credential = GoogleCredential.FromAccessToken("token"),
59+
};
60+
var app = FirebaseApp.Create(options, "1234");
61+
62+
Assert.Throws<ArgumentException>(() => FirebaseAppCheck.Create(app));
63+
}
64+
65+
[Fact]
66+
public async Task CreateTokenFromAppId()
67+
{
68+
FirebaseAppCheck createTokenFromAppId = new FirebaseAppCheck(this.mockCredentialApp);
69+
var token = await createTokenFromAppId.CreateToken(this.appId);
70+
Assert.IsType<string>(token.Token);
71+
Assert.NotNull(token.Token);
72+
Assert.IsType<int>(token.TtlMillis);
73+
Assert.Equal<int>(3600000, token.TtlMillis);
4674
}
4775

4876
[Fact]
49-
public async Task InvalidAppIdCreate()
77+
public async Task CreateTokenFromAppIdAndTtlMillis()
5078
{
51-
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: null));
52-
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: string.Empty));
79+
AppCheckTokenOptions options = new (1800000);
80+
FirebaseAppCheck createTokenFromAppIdAndTtlMillis = new FirebaseAppCheck(this.mockCredentialApp);
81+
82+
var token = await createTokenFromAppIdAndTtlMillis.CreateToken(this.appId, options);
83+
Assert.IsType<string>(token.Token);
84+
Assert.NotNull(token.Token);
85+
Assert.IsType<int>(token.TtlMillis);
86+
Assert.Equal<int>(1800000, token.TtlMillis);
5387
}
5488

5589
[Fact]
56-
public async Task DecodeVerifyToken()
90+
public async Task VerifyToken()
5791
{
58-
string appId = "1234"; // '../resources/appid.txt'
59-
AppCheckToken validToken = await FirebaseAppCheck.CreateToken(appId);
60-
var verifiedToken = FirebaseAppCheck.Decode_and_verify(validToken.Token);
61-
/* Assert.Equal("explicit-project", verifiedToken);*/
92+
FirebaseAppCheck verifyToken = new FirebaseAppCheck(this.mockCredentialApp);
93+
94+
AppCheckToken validToken = await verifyToken.CreateToken(this.appId);
95+
AppCheckVerifyResponse verifiedToken = await verifyToken.VerifyToken(validToken.Token, null);
96+
Assert.Equal("explicit-project", verifiedToken.AppId);
6297
}
6398

6499
[Fact]
65-
public async Task DecodeVerifyTokenInvaild()
100+
public async Task VerifyTokenInvaild()
66101
{
67-
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: null));
68-
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: string.Empty));
102+
FirebaseAppCheck verifyTokenInvaild = new FirebaseAppCheck(this.mockCredentialApp);
103+
104+
await Assert.ThrowsAsync<ArgumentException>(() => verifyTokenInvaild.VerifyToken(null));
105+
await Assert.ThrowsAsync<ArgumentException>(() => verifyTokenInvaild.VerifyToken(string.Empty));
69106
}
70107

71108
public void Dispose()
72109
{
73-
FirebaseAppCheck.Delete();
110+
FirebaseAppCheck.DeleteAll();
74111
}
75112
}
76113
}

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public sealed class FirebaseToken
2626
{
2727
internal FirebaseToken(Args args)
2828
{
29+
this.AppId = args.AppId;
2930
this.Issuer = args.Issuer;
3031
this.Subject = args.Subject;
3132
this.Audience = args.Audience;
@@ -69,6 +70,11 @@ internal FirebaseToken(Args args)
6970
/// </summary>
7071
public string Uid { get; }
7172

73+
/// <summary>
74+
/// Gets the Id of the Firebase .
75+
/// </summary>
76+
public string AppId { get; }
77+
7278
/// <summary>
7379
/// Gets the ID of the tenant the user belongs to, if available. Returns null if the ID
7480
/// token is not scoped to a tenant.
@@ -81,14 +87,20 @@ internal FirebaseToken(Args args)
8187
/// </summary>
8288
public IReadOnlyDictionary<string, object> Claims { get; }
8389

90+
/// <summary>
91+
/// Defined operator string.
92+
/// </summary>
93+
/// <param name="v">FirebaseToken.</param>
8494
public static implicit operator string(FirebaseToken v)
8595
{
8696
throw new NotImplementedException();
8797
}
8898

8999
internal sealed class Args
90100
{
91-
[JsonProperty("iss")]
101+
public string AppId { get; internal set; }
102+
103+
[JsonProperty("app_id")]
92104
internal string Issuer { get; set; }
93105

94106
[JsonProperty("sub")]

FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs renamed to FirebaseAdmin/FirebaseAdmin/Auth/Jwt/AppCheckTokenOptions.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22
using System.Collections.Generic;
33
using System.Text;
44

5-
namespace FirebaseAdmin.Check
5+
namespace FirebaseAdmin.Auth.Jwt
66
{
77
/// <summary>
8-
/// Interface representing App Check token options.
8+
/// Representing App Check token options.
99
/// </summary>
1010
/// <remarks>
1111
/// Initializes a new instance of the <see cref="AppCheckTokenOptions"/> class.
1212
/// </remarks>
13-
/// <param name="v">ttlMillis.</param>
14-
public class AppCheckTokenOptions(int v)
13+
/// <param name="ttl">ttlMillis.</param>
14+
public class AppCheckTokenOptions(int ttl)
1515
{
1616
/// <summary>
1717
/// Gets or sets the length of time, in milliseconds, for which the App Check token will
1818
/// be valid. This value must be between 30 minutes and 7 days, inclusive.
1919
/// </summary>
20-
public int TtlMillis { get; set; } = v;
20+
public int TtlMillis { get; set; } = ttl;
2121
}
2222
}

FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ internal class FirebaseTokenFactory : IDisposable
3939
+ "google.identity.identitytoolkit.v1.IdentityToolkit";
4040

4141
public const int TokenDurationSeconds = 3600;
42+
public const int OneMinuteInSeconds = 60;
43+
public const int OneDayInMillis = 24 * 60 * 60 * 1000;
4244
public static readonly DateTime UnixEpoch = new DateTime(
4345
1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
4446

@@ -58,7 +60,8 @@ internal class FirebaseTokenFactory : IDisposable
5860
"jti",
5961
"nbf",
6062
"nonce",
61-
"sub");
63+
"sub",
64+
"app_id");
6265

6366
internal FirebaseTokenFactory(Args args)
6467
{
@@ -173,14 +176,75 @@ internal async Task<string> CreateCustomTokenAsync(
173176
header, payload, this.Signer, cancellationToken).ConfigureAwait(false);
174177
}
175178

179+
internal async Task<string> CreateCustomTokenAppIdAsync(
180+
string appId,
181+
AppCheckTokenOptions options = null,
182+
CancellationToken cancellationToken = default(CancellationToken))
183+
{
184+
if (string.IsNullOrEmpty(appId))
185+
{
186+
throw new ArgumentException("uid must not be null or empty");
187+
}
188+
else if (appId.Length > 128)
189+
{
190+
throw new ArgumentException("uid must not be longer than 128 characters");
191+
}
192+
193+
var header = new JsonWebSignature.Header()
194+
{
195+
Algorithm = this.Signer.Algorithm,
196+
Type = "JWT",
197+
};
198+
199+
var issued = (int)(this.Clock.UtcNow - UnixEpoch).TotalSeconds / 1000;
200+
var keyId = await this.Signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
201+
var payload = new CustomTokenPayload()
202+
{
203+
AppId = appId,
204+
Issuer = keyId,
205+
Subject = keyId,
206+
Audience = FirebaseAudience,
207+
IssuedAtTimeSeconds = issued,
208+
ExpirationTimeSeconds = issued + (OneMinuteInSeconds * 5),
209+
};
210+
211+
if (options != null)
212+
{
213+
this.ValidateTokenOptions(options);
214+
payload.Ttl = options.TtlMillis.ToString();
215+
}
216+
217+
return await JwtUtils.CreateSignedJwtAsync(
218+
header, payload, this.Signer, cancellationToken).ConfigureAwait(false);
219+
}
220+
221+
internal void ValidateTokenOptions(AppCheckTokenOptions options)
222+
{
223+
if (options.TtlMillis == 0)
224+
{
225+
throw new ArgumentException("TtlMillis must be a duration in milliseconds.");
226+
}
227+
228+
if (options.TtlMillis < OneMinuteInSeconds * 30 || options.TtlMillis > OneDayInMillis * 7)
229+
{
230+
throw new ArgumentException("ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).");
231+
}
232+
}
233+
176234
internal class CustomTokenPayload : JsonWebToken.Payload
177235
{
178236
[JsonPropertyAttribute("uid")]
179237
public string Uid { get; set; }
180238

239+
[JsonPropertyAttribute("app_id")]
240+
public string AppId { get; set; }
241+
181242
[JsonPropertyAttribute("tenant_id")]
182243
public string TenantId { get; set; }
183244

245+
[JsonPropertyAttribute("ttl")]
246+
public string Ttl { get; set; }
247+
184248
[JsonPropertyAttribute("claims")]
185249
public IDictionary<string, object> Claims { get; set; }
186250
}

FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ internal sealed class FirebaseTokenVerifier
3737
private const string SessionCookieCertUrl = "https://www.googleapis.com/identitytoolkit/v3/"
3838
+ "relyingparty/publicKeys";
3939

40+
private const string AppCheckCertUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";
41+
4042
private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
4143
+ "google.identity.identitytoolkit.v1.IdentityToolkit";
4244

@@ -159,6 +161,44 @@ internal static FirebaseTokenVerifier CreateSessionCookieVerifier(
159161
return new FirebaseTokenVerifier(args);
160162
}
161163

164+
internal static FirebaseTokenVerifier CreateAppCheckVerifier(FirebaseApp app)
165+
{
166+
var projectId = app.GetProjectId();
167+
if (string.IsNullOrEmpty(projectId))
168+
{
169+
string errorMessage = "Failed to determine project ID. Initialize the SDK with service account " +
170+
"credentials or set project ID as an app option. Alternatively, set the " +
171+
"GOOGLE_CLOUD_PROJECT environment variable.";
172+
throw new ArgumentException(
173+
"unknown-error",
174+
errorMessage);
175+
}
176+
177+
var keySource = new HttpPublicKeySource(
178+
AppCheckCertUrl, SystemClock.Default, app.Options.HttpClientFactory);
179+
return CreateAppCheckVerifier(projectId, keySource);
180+
}
181+
182+
internal static FirebaseTokenVerifier CreateAppCheckVerifier(
183+
string projectId,
184+
IPublicKeySource keySource,
185+
IClock clock = null)
186+
{
187+
var args = new FirebaseTokenVerifierArgs()
188+
{
189+
ProjectId = projectId,
190+
ShortName = "app check",
191+
Operation = "VerifyAppCheckAsync()",
192+
Url = "https://firebase.google.com/docs/app-check/",
193+
Issuer = "https://firebaseappcheck.googleapis.com/",
194+
Clock = clock,
195+
PublicKeySource = keySource,
196+
InvalidTokenCode = AuthErrorCode.InvalidSessionCookie,
197+
ExpiredTokenCode = AuthErrorCode.ExpiredSessionCookie,
198+
};
199+
return new FirebaseTokenVerifier(args);
200+
}
201+
162202
internal async Task<FirebaseToken> VerifyTokenAsync(
163203
string token, CancellationToken cancellationToken = default(CancellationToken))
164204
{

0 commit comments

Comments
 (0)