-
Notifications
You must be signed in to change notification settings - Fork 132
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
pavlo
committed
Jan 19, 2024
1 parent
8aa2ee6
commit 7760b87
Showing
10 changed files
with
553 additions
and
55 deletions.
There are no files selected for viewing
76 changes: 76 additions & 0 deletions
76
FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Threading.Tasks; | ||
using FirebaseAdmin; | ||
using FirebaseAdmin.Auth; | ||
using FirebaseAdmin.Check; | ||
using Google.Apis.Auth.OAuth2; | ||
using Xunit; | ||
|
||
namespace FirebaseAdmin.Tests | ||
{ | ||
public class FirebaseAppCheckTests : IDisposable | ||
{ | ||
[Fact] | ||
public async Task CreateTokenFromAppId() | ||
{ | ||
string filePath = @"C:\path\to\your\file.txt"; | ||
string fileContent = File.ReadAllText(filePath); | ||
string[] appIds = fileContent.Split(','); | ||
foreach (string appId in appIds) | ||
{ | ||
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); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task CreateTokenFromAppIdAndTtlMillis() | ||
{ | ||
string filePath = @"C:\path\to\your\file.txt"; | ||
string fileContent = File.ReadAllText(filePath); | ||
string[] appIds = fileContent.Split(','); | ||
foreach (string appId in appIds) | ||
{ | ||
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); | ||
} | ||
} | ||
|
||
[Fact] | ||
public async Task InvalidAppIdCreate() | ||
{ | ||
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: null)); | ||
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: string.Empty)); | ||
} | ||
|
||
[Fact] | ||
public async Task DecodeVerifyToken() | ||
{ | ||
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);*/ | ||
} | ||
|
||
[Fact] | ||
public async Task DecodeVerifyTokenInvaild() | ||
{ | ||
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: null)); | ||
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: string.Empty)); | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
FirebaseAppCheck.Delete(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
1234,project,Appcheck |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.Net.Http; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace FirebaseAdmin.Check | ||
{ | ||
internal class AppCheckApiClient | ||
{ | ||
private const string ApiUrlFormat = "https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken"; | ||
private readonly FirebaseApp app; | ||
private string projectId; | ||
private string appId; | ||
|
||
public AppCheckApiClient(FirebaseApp value) | ||
{ | ||
if (value == null || value.Options == null) | ||
{ | ||
throw new ArgumentException("Argument passed to admin.appCheck() must be a valid Firebase app instance."); | ||
} | ||
|
||
this.app = value; | ||
this.projectId = this.app.Options.ProjectId; | ||
} | ||
|
||
public AppCheckApiClient(string appId) | ||
{ | ||
this.appId = appId; | ||
} | ||
|
||
public async Task<AppCheckToken> ExchangeToken(string customToken) | ||
{ | ||
if (customToken == null) | ||
{ | ||
throw new ArgumentException("First argument passed to customToken must be a valid Firebase app instance."); | ||
} | ||
|
||
if (this.appId == null) | ||
{ | ||
throw new ArgumentException("Second argument passed to appId must be a valid Firebase app instance."); | ||
} | ||
|
||
var url = this.GetUrl(this.appId); | ||
var request = new HttpRequestMessage() | ||
{ | ||
Method = HttpMethod.Post, | ||
RequestUri = new Uri(url), | ||
Content = new StringContent(customToken), | ||
}; | ||
request.Headers.Add("X-Firebase-Client", "fire-admin-node/${utils.getSdkVersion()}"); | ||
var httpClient = new HttpClient(); | ||
var response = await httpClient.SendAsync(request).ConfigureAwait(false); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
throw new ArgumentException("Error exchanging token."); | ||
} | ||
|
||
var responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); | ||
string tokenValue = responseData["data"]["token"].ToString(); | ||
int ttlValue = int.Parse(responseData["data"]["ttl"].ToString()); | ||
AppCheckToken appCheckToken = new (tokenValue, ttlValue); | ||
return appCheckToken; | ||
} | ||
|
||
private string GetUrl(string appId) | ||
{ | ||
if (string.IsNullOrEmpty(this.projectId)) | ||
{ | ||
this.projectId = this.app.GetProjectId(); | ||
} | ||
|
||
if (string.IsNullOrEmpty(this.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 urlParams = new Dictionary<string, string> | ||
{ | ||
{ "projectId", this.projectId }, | ||
{ "appId", appId }, | ||
}; | ||
string baseUrl = this.FormatString(ApiUrlFormat, urlParams); | ||
return baseUrl; | ||
} | ||
|
||
private string FormatString(string str, Dictionary<string, string> urlParams) | ||
{ | ||
string formatted = str; | ||
foreach (var key in urlParams.Keys) | ||
{ | ||
formatted = Regex.Replace(formatted, $"{{{key}}}", urlParams[key]); | ||
} | ||
|
||
return formatted; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace FirebaseAdmin.Check | ||
{ | ||
/// <summary> | ||
/// Interface representing an App Check token. | ||
/// </summary> | ||
public class AppCheckService | ||
{ | ||
private const long OneMinuteInMillis = 60 * 1000; // 60,000 | ||
private const long OneDayInMillis = 24 * 60 * OneMinuteInMillis; // 1,440,000 | ||
|
||
/// <summary> | ||
/// Interface representing an App Check token. | ||
/// </summary> | ||
/// <param name="options"> IDictionary string, object .</param> | ||
/// <returns>IDictionary string object .</returns> | ||
public static Dictionary<string, object> ValidateTokenOptions(AppCheckTokenOptions options) | ||
{ | ||
if (options == null) | ||
{ | ||
throw new FirebaseAppCheckError( | ||
"invalid-argument", | ||
"AppCheckTokenOptions must be a non-null object."); | ||
} | ||
|
||
if (options.TtlMillis > 0) | ||
{ | ||
long ttlMillis = options.TtlMillis; | ||
if (ttlMillis < (OneMinuteInMillis * 30) || ttlMillis > (OneDayInMillis * 7)) | ||
{ | ||
throw new FirebaseAppCheckError( | ||
"invalid-argument", | ||
"ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive)."); | ||
} | ||
|
||
return new Dictionary<string, object> { { "ttl", TransformMillisecondsToSecondsString(ttlMillis) } }; | ||
} | ||
|
||
return new Dictionary<string, object>(); | ||
} | ||
|
||
private static string TransformMillisecondsToSecondsString(long milliseconds) | ||
{ | ||
return (milliseconds / 1000).ToString(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace FirebaseAdmin.Check | ||
{ | ||
/// <summary> | ||
/// Interface representing an App Check token. | ||
/// </summary> | ||
/// <remarks> | ||
/// Initializes a new instance of the <see cref="AppCheckToken"/> class. | ||
/// </remarks> | ||
/// <param name="tokenValue">Generator from custom token.</param> | ||
/// <param name="ttlValue">TTl value .</param> | ||
public class AppCheckToken(string tokenValue, int ttlValue) | ||
{ | ||
/// <summary> | ||
/// Gets the Firebase App Check token. | ||
/// </summary> | ||
public string Token { get; } = tokenValue; | ||
|
||
/// <summary> | ||
/// Gets or sets the time-to-live duration of the token in milliseconds. | ||
/// </summary> | ||
public int TtlMillis { get; set; } = ttlValue; | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
using FirebaseAdmin.Auth.Jwt; | ||
using FirebaseAdmin.Check; | ||
using Google.Apis.Auth; | ||
using Google.Apis.Auth.OAuth2; | ||
using Google.Apis.Json; | ||
|
||
namespace FirebaseAdmin.Check | ||
{ | ||
/// <summary> | ||
/// Initializes static members of the <see cref="AppCheckTokenGernerator"/> class. | ||
/// </summary> | ||
public class AppCheckTokenGernerator | ||
{ | ||
private static readonly string AppCheckAudience = "https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService"; | ||
private readonly CyptoSigner signer; | ||
private string appId; | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="AppCheckTokenGernerator"/> class. | ||
/// </summary> | ||
/// <param name="appId">FirebaseApp Id.</param> | ||
public AppCheckTokenGernerator(string appId) | ||
{ | ||
this.appId = appId; | ||
} | ||
|
||
/// <summary> | ||
/// Initializes static members of the <see cref="AppCheckTokenGernerator"/> class. | ||
/// </summary> | ||
/// <param name="appId"> FirebaseApp Id.</param> | ||
/// <param name="options"> FirebaseApp AppCheckTokenOptions.</param> | ||
/// <returns> Created token.</returns> | ||
public static string CreateCustomToken(string appId, AppCheckTokenOptions options) | ||
{ | ||
var customOptions = new Dictionary<string, string>(); | ||
|
||
if (string.IsNullOrEmpty(appId)) | ||
{ | ||
throw new ArgumentNullException(nameof(appId)); | ||
} | ||
|
||
if (options == null) | ||
{ | ||
customOptions.Add(AppCheckService.ValidateTokenOptions(options)); | ||
} | ||
|
||
CyptoSigner signer = new (appId); | ||
string account = signer.GetAccountId(); | ||
|
||
var header = new Dictionary<string, string>() | ||
{ | ||
{ "alg", "RS256" }, | ||
{ "typ", "JWT" }, | ||
}; | ||
var iat = Math.Floor(DateTime.now() / 1000); | ||
var payload = new Dictionary<string, string>() | ||
{ | ||
{ "iss", account }, | ||
{ "sub", account }, | ||
{ "app_id", appId }, | ||
{ "aud", AppCheckAudience }, | ||
{ "exp", iat + 300 }, | ||
{ "iat", iat }, | ||
}; | ||
|
||
foreach (var each in customOptions) | ||
{ | ||
payload.Add(each.Key, each.Value); | ||
} | ||
|
||
string token = Encode(header) + Encode(payload); | ||
return token; | ||
} | ||
|
||
private static string Encode(object obj) | ||
{ | ||
var json = NewtonsoftJsonSerializer.Instance.Serialize(obj); | ||
return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(json)); | ||
} | ||
|
||
private static string UrlSafeBase64Encode(byte[] bytes) | ||
{ | ||
var base64Value = Convert.ToBase64String(bytes); | ||
return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_'); | ||
} | ||
} | ||
} |
Oops, something went wrong.