Skip to content

Commit 7760b87

Browse files
author
pavlo
committed
feature/AppcheckTest
1 parent 8aa2ee6 commit 7760b87

File tree

10 files changed

+553
-55
lines changed

10 files changed

+553
-55
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Threading.Tasks;
5+
using FirebaseAdmin;
6+
using FirebaseAdmin.Auth;
7+
using FirebaseAdmin.Check;
8+
using Google.Apis.Auth.OAuth2;
9+
using Xunit;
10+
11+
namespace FirebaseAdmin.Tests
12+
{
13+
public class FirebaseAppCheckTests : IDisposable
14+
{
15+
[Fact]
16+
public async Task CreateTokenFromAppId()
17+
{
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)
22+
{
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+
}
29+
}
30+
31+
[Fact]
32+
public async Task CreateTokenFromAppIdAndTtlMillis()
33+
{
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)
38+
{
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+
}
46+
}
47+
48+
[Fact]
49+
public async Task InvalidAppIdCreate()
50+
{
51+
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: null));
52+
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.CreateToken(appId: string.Empty));
53+
}
54+
55+
[Fact]
56+
public async Task DecodeVerifyToken()
57+
{
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);*/
62+
}
63+
64+
[Fact]
65+
public async Task DecodeVerifyTokenInvaild()
66+
{
67+
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: null));
68+
await Assert.ThrowsAsync<ArgumentException>(() => FirebaseAppCheck.Decode_and_verify(token: string.Empty));
69+
}
70+
71+
public void Dispose()
72+
{
73+
FirebaseAppCheck.Delete();
74+
}
75+
}
76+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1234,project,Appcheck

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System;
1516
using System.Collections.Generic;
1617
using Newtonsoft.Json;
1718

@@ -80,6 +81,11 @@ internal FirebaseToken(Args args)
8081
/// </summary>
8182
public IReadOnlyDictionary<string, object> Claims { get; }
8283

84+
public static implicit operator string(FirebaseToken v)
85+
{
86+
throw new NotImplementedException();
87+
}
88+
8389
internal sealed class Args
8490
{
8591
[JsonProperty("iss")]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Net.Http;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Newtonsoft.Json.Linq;
10+
11+
namespace FirebaseAdmin.Check
12+
{
13+
internal class AppCheckApiClient
14+
{
15+
private const string ApiUrlFormat = "https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken";
16+
private readonly FirebaseApp app;
17+
private string projectId;
18+
private string appId;
19+
20+
public AppCheckApiClient(FirebaseApp value)
21+
{
22+
if (value == null || value.Options == null)
23+
{
24+
throw new ArgumentException("Argument passed to admin.appCheck() must be a valid Firebase app instance.");
25+
}
26+
27+
this.app = value;
28+
this.projectId = this.app.Options.ProjectId;
29+
}
30+
31+
public AppCheckApiClient(string appId)
32+
{
33+
this.appId = appId;
34+
}
35+
36+
public async Task<AppCheckToken> ExchangeToken(string customToken)
37+
{
38+
if (customToken == null)
39+
{
40+
throw new ArgumentException("First argument passed to customToken must be a valid Firebase app instance.");
41+
}
42+
43+
if (this.appId == null)
44+
{
45+
throw new ArgumentException("Second argument passed to appId must be a valid Firebase app instance.");
46+
}
47+
48+
var url = this.GetUrl(this.appId);
49+
var request = new HttpRequestMessage()
50+
{
51+
Method = HttpMethod.Post,
52+
RequestUri = new Uri(url),
53+
Content = new StringContent(customToken),
54+
};
55+
request.Headers.Add("X-Firebase-Client", "fire-admin-node/${utils.getSdkVersion()}");
56+
var httpClient = new HttpClient();
57+
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
58+
if (!response.IsSuccessStatusCode)
59+
{
60+
throw new ArgumentException("Error exchanging token.");
61+
}
62+
63+
var responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
64+
string tokenValue = responseData["data"]["token"].ToString();
65+
int ttlValue = int.Parse(responseData["data"]["ttl"].ToString());
66+
AppCheckToken appCheckToken = new (tokenValue, ttlValue);
67+
return appCheckToken;
68+
}
69+
70+
private string GetUrl(string appId)
71+
{
72+
if (string.IsNullOrEmpty(this.projectId))
73+
{
74+
this.projectId = this.app.GetProjectId();
75+
}
76+
77+
if (string.IsNullOrEmpty(this.projectId))
78+
{
79+
string errorMessage = "Failed to determine project ID. Initialize the SDK with service account " +
80+
"credentials or set project ID as an app option. Alternatively, set the " +
81+
"GOOGLE_CLOUD_PROJECT environment variable.";
82+
throw new ArgumentException(
83+
"unknown-error",
84+
errorMessage);
85+
}
86+
87+
var urlParams = new Dictionary<string, string>
88+
{
89+
{ "projectId", this.projectId },
90+
{ "appId", appId },
91+
};
92+
string baseUrl = this.FormatString(ApiUrlFormat, urlParams);
93+
return baseUrl;
94+
}
95+
96+
private string FormatString(string str, Dictionary<string, string> urlParams)
97+
{
98+
string formatted = str;
99+
foreach (var key in urlParams.Keys)
100+
{
101+
formatted = Regex.Replace(formatted, $"{{{key}}}", urlParams[key]);
102+
}
103+
104+
return formatted;
105+
}
106+
}
107+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace FirebaseAdmin.Check
6+
{
7+
/// <summary>
8+
/// Interface representing an App Check token.
9+
/// </summary>
10+
public class AppCheckService
11+
{
12+
private const long OneMinuteInMillis = 60 * 1000; // 60,000
13+
private const long OneDayInMillis = 24 * 60 * OneMinuteInMillis; // 1,440,000
14+
15+
/// <summary>
16+
/// Interface representing an App Check token.
17+
/// </summary>
18+
/// <param name="options"> IDictionary string, object .</param>
19+
/// <returns>IDictionary string object .</returns>
20+
public static Dictionary<string, object> ValidateTokenOptions(AppCheckTokenOptions options)
21+
{
22+
if (options == null)
23+
{
24+
throw new FirebaseAppCheckError(
25+
"invalid-argument",
26+
"AppCheckTokenOptions must be a non-null object.");
27+
}
28+
29+
if (options.TtlMillis > 0)
30+
{
31+
long ttlMillis = options.TtlMillis;
32+
if (ttlMillis < (OneMinuteInMillis * 30) || ttlMillis > (OneDayInMillis * 7))
33+
{
34+
throw new FirebaseAppCheckError(
35+
"invalid-argument",
36+
"ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).");
37+
}
38+
39+
return new Dictionary<string, object> { { "ttl", TransformMillisecondsToSecondsString(ttlMillis) } };
40+
}
41+
42+
return new Dictionary<string, object>();
43+
}
44+
45+
private static string TransformMillisecondsToSecondsString(long milliseconds)
46+
{
47+
return (milliseconds / 1000).ToString();
48+
}
49+
}
50+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace FirebaseAdmin.Check
6+
{
7+
/// <summary>
8+
/// Interface representing an App Check token.
9+
/// </summary>
10+
/// <remarks>
11+
/// Initializes a new instance of the <see cref="AppCheckToken"/> class.
12+
/// </remarks>
13+
/// <param name="tokenValue">Generator from custom token.</param>
14+
/// <param name="ttlValue">TTl value .</param>
15+
public class AppCheckToken(string tokenValue, int ttlValue)
16+
{
17+
/// <summary>
18+
/// Gets the Firebase App Check token.
19+
/// </summary>
20+
public string Token { get; } = tokenValue;
21+
22+
/// <summary>
23+
/// Gets or sets the time-to-live duration of the token in milliseconds.
24+
/// </summary>
25+
public int TtlMillis { get; set; } = ttlValue;
26+
}
27+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using FirebaseAdmin.Auth.Jwt;
5+
using FirebaseAdmin.Check;
6+
using Google.Apis.Auth;
7+
using Google.Apis.Auth.OAuth2;
8+
using Google.Apis.Json;
9+
10+
namespace FirebaseAdmin.Check
11+
{
12+
/// <summary>
13+
/// Initializes static members of the <see cref="AppCheckTokenGernerator"/> class.
14+
/// </summary>
15+
public class AppCheckTokenGernerator
16+
{
17+
private static readonly string AppCheckAudience = "https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService";
18+
private readonly CyptoSigner signer;
19+
private string appId;
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="AppCheckTokenGernerator"/> class.
23+
/// </summary>
24+
/// <param name="appId">FirebaseApp Id.</param>
25+
public AppCheckTokenGernerator(string appId)
26+
{
27+
this.appId = appId;
28+
}
29+
30+
/// <summary>
31+
/// Initializes static members of the <see cref="AppCheckTokenGernerator"/> class.
32+
/// </summary>
33+
/// <param name="appId"> FirebaseApp Id.</param>
34+
/// <param name="options"> FirebaseApp AppCheckTokenOptions.</param>
35+
/// <returns> Created token.</returns>
36+
public static string CreateCustomToken(string appId, AppCheckTokenOptions options)
37+
{
38+
var customOptions = new Dictionary<string, string>();
39+
40+
if (string.IsNullOrEmpty(appId))
41+
{
42+
throw new ArgumentNullException(nameof(appId));
43+
}
44+
45+
if (options == null)
46+
{
47+
customOptions.Add(AppCheckService.ValidateTokenOptions(options));
48+
}
49+
50+
CyptoSigner signer = new (appId);
51+
string account = signer.GetAccountId();
52+
53+
var header = new Dictionary<string, string>()
54+
{
55+
{ "alg", "RS256" },
56+
{ "typ", "JWT" },
57+
};
58+
var iat = Math.Floor(DateTime.now() / 1000);
59+
var payload = new Dictionary<string, string>()
60+
{
61+
{ "iss", account },
62+
{ "sub", account },
63+
{ "app_id", appId },
64+
{ "aud", AppCheckAudience },
65+
{ "exp", iat + 300 },
66+
{ "iat", iat },
67+
};
68+
69+
foreach (var each in customOptions)
70+
{
71+
payload.Add(each.Key, each.Value);
72+
}
73+
74+
string token = Encode(header) + Encode(payload);
75+
return token;
76+
}
77+
78+
private static string Encode(object obj)
79+
{
80+
var json = NewtonsoftJsonSerializer.Instance.Serialize(obj);
81+
return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(json));
82+
}
83+
84+
private static string UrlSafeBase64Encode(byte[] bytes)
85+
{
86+
var base64Value = Convert.ToBase64String(bytes);
87+
return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_');
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)