Skip to content

Commit

Permalink
feature/app-check/test
Browse files Browse the repository at this point in the history
  • Loading branch information
pavlo committed Jan 23, 2024
1 parent 7760b87 commit bc8360f
Show file tree
Hide file tree
Showing 16 changed files with 525 additions and 432 deletions.
111 changes: 74 additions & 37 deletions FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
Original file line number Diff line number Diff line change
@@ -1,76 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Reflection.Metadata.Ecma335;
using System.Threading.Tasks;
using FirebaseAdmin;
using FirebaseAdmin.Auth;
using FirebaseAdmin.Auth.Jwt;
using FirebaseAdmin.Auth.Tests;
using FirebaseAdmin.Check;
using Google.Apis.Auth.OAuth2;
using Moq;
using Newtonsoft.Json.Linq;
using Xunit;

namespace FirebaseAdmin.Tests
{
public class FirebaseAppCheckTests : IDisposable
{
[Fact]
public async Task CreateTokenFromAppId()
private readonly string appId = "1:1234:android:1234";
private FirebaseApp mockCredentialApp;

public FirebaseAppCheckTests()
{
string filePath = @"C:\path\to\your\file.txt";
string fileContent = File.ReadAllText(filePath);
string[] appIds = fileContent.Split(',');
foreach (string appId in appIds)
var credential = GoogleCredential.FromFile("./resources/service_account.json");
var options = new AppOptions()
{
var token = await FirebaseAppCheck.CreateToken(appId);
Assert.IsType<string>(token.Token);
Assert.NotNull(token.Token);
Assert.IsType<int>(token.TtlMillis);
Assert.Equal<int>(3600000, token.TtlMillis);
}
Credential = credential,
};
this.mockCredentialApp = FirebaseApp.Create(options);
}

[Fact]
public async Task CreateTokenFromAppIdAndTtlMillis()
public void CreateAppCheck()
{
FirebaseAppCheck withoutAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
Assert.NotNull(withoutAppIdCreate);
}

[Fact]
public async Task InvalidAppIdCreateToken()
{
FirebaseAppCheck invalidAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);

await Assert.ThrowsAsync<ArgumentException>(() => invalidAppIdCreate.CreateToken(appId: null));
await Assert.ThrowsAsync<ArgumentException>(() => invalidAppIdCreate.CreateToken(appId: string.Empty));
}

[Fact]
public void WithoutProjectIDCreate()
{
string filePath = @"C:\path\to\your\file.txt";
string fileContent = File.ReadAllText(filePath);
string[] appIds = fileContent.Split(',');
foreach (string appId in appIds)
// Project ID not set in the environment.
Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", null);
Environment.SetEnvironmentVariable("GCLOUD_PROJECT", null);

var options = new AppOptions()
{
AppCheckTokenOptions options = new (1800000);
var token = await FirebaseAppCheck.CreateToken(appId, options);
Assert.IsType<string>(token.Token);
Assert.NotNull(token.Token);
Assert.IsType<int>(token.TtlMillis);
Assert.Equal<int>(1800000, token.TtlMillis);
}
Credential = GoogleCredential.FromAccessToken("token"),
};
var app = FirebaseApp.Create(options, "1234");

Assert.Throws<ArgumentException>(() => FirebaseAppCheck.Create(app));
}

[Fact]
public async Task CreateTokenFromAppId()
{
FirebaseAppCheck createTokenFromAppId = new FirebaseAppCheck(this.mockCredentialApp);
var token = await createTokenFromAppId.CreateToken(this.appId);
Assert.IsType<string>(token.Token);
Assert.NotNull(token.Token);
Assert.IsType<int>(token.TtlMillis);
Assert.Equal<int>(3600000, token.TtlMillis);
}

[Fact]
public async Task InvalidAppIdCreate()
public async Task CreateTokenFromAppIdAndTtlMillis()
{
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: null));
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: string.Empty));
AppCheckTokenOptions options = new (1800000);
FirebaseAppCheck createTokenFromAppIdAndTtlMillis = new FirebaseAppCheck(this.mockCredentialApp);

var token = await createTokenFromAppIdAndTtlMillis.CreateToken(this.appId, options);
Assert.IsType<string>(token.Token);
Assert.NotNull(token.Token);
Assert.IsType<int>(token.TtlMillis);
Assert.Equal<int>(1800000, token.TtlMillis);
}

[Fact]
public async Task DecodeVerifyToken()
public async Task VerifyToken()
{
string appId = "1234"; // '../resources/appid.txt'
AppCheckToken validToken = await FirebaseAppCheck.CreateToken(appId);
var verifiedToken = FirebaseAppCheck.Decode_and_verify(validToken.Token);
/* Assert.Equal("explicit-project", verifiedToken);*/
FirebaseAppCheck verifyToken = new FirebaseAppCheck(this.mockCredentialApp);

AppCheckToken validToken = await verifyToken.CreateToken(this.appId);
AppCheckVerifyResponse verifiedToken = await verifyToken.VerifyToken(validToken.Token, null);
Assert.Equal("explicit-project", verifiedToken.AppId);
}

[Fact]
public async Task DecodeVerifyTokenInvaild()
public async Task VerifyTokenInvaild()
{
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: null));
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: string.Empty));
FirebaseAppCheck verifyTokenInvaild = new FirebaseAppCheck(this.mockCredentialApp);

await Assert.ThrowsAsync<ArgumentException>(() => verifyTokenInvaild.VerifyToken(null));
await Assert.ThrowsAsync<ArgumentException>(() => verifyTokenInvaild.VerifyToken(string.Empty));
}

public void Dispose()
{
FirebaseAppCheck.Delete();
FirebaseAppCheck.DeleteAll();
}
}
}
14 changes: 13 additions & 1 deletion FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public sealed class FirebaseToken
{
internal FirebaseToken(Args args)
{
this.AppId = args.AppId;
this.Issuer = args.Issuer;
this.Subject = args.Subject;
this.Audience = args.Audience;
Expand Down Expand Up @@ -69,6 +70,11 @@ internal FirebaseToken(Args args)
/// </summary>
public string Uid { get; }

/// <summary>
/// Gets the Id of the Firebase .
/// </summary>
public string AppId { get; }

/// <summary>
/// Gets the ID of the tenant the user belongs to, if available. Returns null if the ID
/// token is not scoped to a tenant.
Expand All @@ -81,14 +87,20 @@ internal FirebaseToken(Args args)
/// </summary>
public IReadOnlyDictionary<string, object> Claims { get; }

/// <summary>
/// Defined operator string.
/// </summary>
/// <param name="v">FirebaseToken.</param>
public static implicit operator string(FirebaseToken v)
{
throw new NotImplementedException();
}

internal sealed class Args
{
[JsonProperty("iss")]
public string AppId { get; internal set; }

[JsonProperty("app_id")]
internal string Issuer { get; set; }

[JsonProperty("sub")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
using System.Collections.Generic;
using System.Text;

namespace FirebaseAdmin.Check
namespace FirebaseAdmin.Auth.Jwt
{
/// <summary>
/// Interface representing App Check token options.
/// Representing App Check token options.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="AppCheckTokenOptions"/> class.
/// </remarks>
/// <param name="v">ttlMillis.</param>
public class AppCheckTokenOptions(int v)
/// <param name="ttl">ttlMillis.</param>
public class AppCheckTokenOptions(int ttl)
{
/// <summary>
/// Gets or sets the length of time, in milliseconds, for which the App Check token will
/// be valid. This value must be between 30 minutes and 7 days, inclusive.
/// </summary>
public int TtlMillis { get; set; } = v;
public int TtlMillis { get; set; } = ttl;
}
}
66 changes: 65 additions & 1 deletion FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ internal class FirebaseTokenFactory : IDisposable
+ "google.identity.identitytoolkit.v1.IdentityToolkit";

public const int TokenDurationSeconds = 3600;
public const int OneMinuteInSeconds = 60;
public const int OneDayInMillis = 24 * 60 * 60 * 1000;
public static readonly DateTime UnixEpoch = new DateTime(
1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

Expand All @@ -58,7 +60,8 @@ internal class FirebaseTokenFactory : IDisposable
"jti",
"nbf",
"nonce",
"sub");
"sub",
"app_id");

internal FirebaseTokenFactory(Args args)
{
Expand Down Expand Up @@ -173,14 +176,75 @@ internal async Task<string> CreateCustomTokenAsync(
header, payload, this.Signer, cancellationToken).ConfigureAwait(false);
}

internal async Task<string> CreateCustomTokenAppIdAsync(
string appId,
AppCheckTokenOptions options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
if (string.IsNullOrEmpty(appId))
{
throw new ArgumentException("uid must not be null or empty");
}
else if (appId.Length > 128)
{
throw new ArgumentException("uid must not be longer than 128 characters");
}

var header = new JsonWebSignature.Header()
{
Algorithm = this.Signer.Algorithm,
Type = "JWT",
};

var issued = (int)(this.Clock.UtcNow - UnixEpoch).TotalSeconds / 1000;
var keyId = await this.Signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
var payload = new CustomTokenPayload()
{
AppId = appId,
Issuer = keyId,
Subject = keyId,
Audience = FirebaseAudience,
IssuedAtTimeSeconds = issued,
ExpirationTimeSeconds = issued + (OneMinuteInSeconds * 5),
};

if (options != null)
{
this.ValidateTokenOptions(options);
payload.Ttl = options.TtlMillis.ToString();
}

return await JwtUtils.CreateSignedJwtAsync(
header, payload, this.Signer, cancellationToken).ConfigureAwait(false);
}

internal void ValidateTokenOptions(AppCheckTokenOptions options)
{
if (options.TtlMillis == 0)
{
throw new ArgumentException("TtlMillis must be a duration in milliseconds.");
}

if (options.TtlMillis < OneMinuteInSeconds * 30 || options.TtlMillis > OneDayInMillis * 7)
{
throw new ArgumentException("ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).");
}
}

internal class CustomTokenPayload : JsonWebToken.Payload
{
[JsonPropertyAttribute("uid")]
public string Uid { get; set; }

[JsonPropertyAttribute("app_id")]
public string AppId { get; set; }

[JsonPropertyAttribute("tenant_id")]
public string TenantId { get; set; }

[JsonPropertyAttribute("ttl")]
public string Ttl { get; set; }

[JsonPropertyAttribute("claims")]
public IDictionary<string, object> Claims { get; set; }
}
Expand Down
40 changes: 40 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ internal sealed class FirebaseTokenVerifier
private const string SessionCookieCertUrl = "https://www.googleapis.com/identitytoolkit/v3/"
+ "relyingparty/publicKeys";

private const string AppCheckCertUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";

private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
+ "google.identity.identitytoolkit.v1.IdentityToolkit";

Expand Down Expand Up @@ -159,6 +161,44 @@ internal static FirebaseTokenVerifier CreateSessionCookieVerifier(
return new FirebaseTokenVerifier(args);
}

internal static FirebaseTokenVerifier CreateAppCheckVerifier(FirebaseApp app)
{
var projectId = app.GetProjectId();
if (string.IsNullOrEmpty(projectId))
{
string errorMessage = "Failed to determine project ID. Initialize the SDK with service account " +
"credentials or set project ID as an app option. Alternatively, set the " +
"GOOGLE_CLOUD_PROJECT environment variable.";
throw new ArgumentException(
"unknown-error",
errorMessage);
}

var keySource = new HttpPublicKeySource(
AppCheckCertUrl, SystemClock.Default, app.Options.HttpClientFactory);
return CreateAppCheckVerifier(projectId, keySource);
}

internal static FirebaseTokenVerifier CreateAppCheckVerifier(
string projectId,
IPublicKeySource keySource,
IClock clock = null)
{
var args = new FirebaseTokenVerifierArgs()
{
ProjectId = projectId,
ShortName = "app check",
Operation = "VerifyAppCheckAsync()",
Url = "https://firebase.google.com/docs/app-check/",
Issuer = "https://firebaseappcheck.googleapis.com/",
Clock = clock,
PublicKeySource = keySource,
InvalidTokenCode = AuthErrorCode.InvalidSessionCookie,
ExpiredTokenCode = AuthErrorCode.ExpiredSessionCookie,
};
return new FirebaseTokenVerifier(args);
}

internal async Task<FirebaseToken> VerifyTokenAsync(
string token, CancellationToken cancellationToken = default(CancellationToken))
{
Expand Down
Loading

0 comments on commit bc8360f

Please sign in to comment.