Skip to content

Commit 8aa2ee6

Browse files
author
pavlo
committed
migrate appcheck
1 parent e4f5a55 commit 8aa2ee6

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Net.Http;
7+
using System.Security.Cryptography;
8+
using System.Security.Cryptography.X509Certificates;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
using FirebaseAdmin.Auth;
12+
using FirebaseAdmin.Auth.Jwt;
13+
using Google.Apis.Auth;
14+
using Newtonsoft.Json;
15+
using RSAKey = System.Security.Cryptography.RSA;
16+
17+
namespace FirebaseAdmin
18+
{
19+
internal class FirebaseAppCheck
20+
{
21+
private readonly string appCheckIssuer = "https://firebaseappcheck.googleapis.com/";
22+
private readonly string jwksUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";
23+
private Dictionary<string, FirebaseToken> appCheck = new Dictionary<string, FirebaseToken>();
24+
private string projectId;
25+
private string scopedProjectId;
26+
private List<Auth.Jwt.PublicKey> cachedKeys;
27+
private IReadOnlyList<string> standardClaims =
28+
ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
29+
30+
private FirebaseAppCheck(FirebaseApp app)
31+
{
32+
this.scopedProjectId = "projects/" + this.projectId;
33+
FirebaseTokenVerifier tokenVerifier = FirebaseTokenVerifier.CreateIdTokenVerifier(app);
34+
this.projectId = tokenVerifier.ProjectId;
35+
}
36+
37+
public static async Task<FirebaseAppCheck> CreateAsync(FirebaseApp app)
38+
{
39+
FirebaseAppCheck appCheck = new (app);
40+
bool result = await appCheck.Init().ConfigureAwait(false); // If Init fails, handle it accordingly
41+
if (!result)
42+
{
43+
return appCheck;
44+
throw new ArgumentException("Error App check initilaization ");
45+
}
46+
47+
return appCheck;
48+
}
49+
50+
public async Task<bool> Init()
51+
{
52+
try
53+
{
54+
using var client = new HttpClient();
55+
HttpResponseMessage response = await client.GetAsync(this.jwksUrl).ConfigureAwait(false);
56+
if (response.StatusCode == HttpStatusCode.OK)
57+
{
58+
string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
59+
KeysRoot keysRoot = JsonConvert.DeserializeObject<KeysRoot>(responseString);
60+
foreach (Key key in keysRoot.Keys)
61+
{
62+
var x509cert = new X509Certificate2(Encoding.UTF8.GetBytes(key.N));
63+
RSAKey rsa = x509cert.GetRSAPublicKey();
64+
this.cachedKeys.Add(new Auth.Jwt.PublicKey(key.Kid, rsa));
65+
}
66+
67+
this.cachedKeys.ToImmutableList();
68+
return true;
69+
}
70+
else
71+
{
72+
throw new ArgumentException("Error Http request JwksUrl");
73+
}
74+
}
75+
catch (Exception exception)
76+
{
77+
throw new ArgumentException("Error Http request", exception);
78+
}
79+
}
80+
81+
public async Task<Dictionary<string, FirebaseToken>> VerifyTokenAsync(string token)
82+
{
83+
if (string.IsNullOrEmpty(token))
84+
{
85+
throw new ArgumentException("App check token " + token + " must be a non - empty string.");
86+
}
87+
88+
try
89+
{
90+
FirebaseToken verified_claims = await this.Decode_and_verify(token).ConfigureAwait(false);
91+
Dictionary<string, FirebaseToken> appchecks = new ();
92+
appchecks.Add(this.projectId, verified_claims);
93+
return appchecks;
94+
}
95+
catch (Exception exception)
96+
{
97+
throw new ArgumentException("Verifying App Check token failed. Error:", exception);
98+
}
99+
}
100+
101+
private Task<FirebaseToken> Decode_and_verify(string token)
102+
{
103+
string[] segments = token.Split('.');
104+
if (segments.Length != 3)
105+
{
106+
throw new ArgumentException("Incorrect number of segments in Token");
107+
}
108+
109+
var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
110+
var payload = JwtUtils.Decode<FirebaseToken.Args>(segments[1]);
111+
var projectIdMessage = $"Make sure the comes from the same Firebase "
112+
+ "project as the credential used to initialize this SDK.";
113+
string issuer = this.appCheckIssuer + this.projectId;
114+
string error = null;
115+
if (header.Algorithm != "RS256")
116+
{
117+
error = "The provided App Check token has incorrect algorithm. Expected RS256 but got '"
118+
+ header.Algorithm + "'";
119+
}
120+
else if (payload.Audience.Contains(this.scopedProjectId))
121+
{
122+
error = "The provided App Check token has incorrect 'aud' (audience) claim.Expected "
123+
+ $"{this.scopedProjectId} but got {payload.Audience}. {projectIdMessage} ";
124+
}
125+
else if (!(payload.Issuer is not null) || !payload.Issuer.StartsWith(this.appCheckIssuer))
126+
{
127+
error = "The provided App Check token has incorrect 'iss' (issuer) claim.";
128+
}
129+
else if (string.IsNullOrEmpty(payload.Subject))
130+
{
131+
error = $"Firebase has no or empty subject (sub) claim.";
132+
}
133+
134+
if (error != null)
135+
{
136+
throw new ArgumentException("invalid - argument" + error);
137+
}
138+
139+
byte[] hash;
140+
using (var hashAlg = SHA256.Create())
141+
{
142+
hash = hashAlg.ComputeHash(
143+
Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
144+
}
145+
146+
var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
147+
var verified = this.cachedKeys.Any(key =>
148+
key.Id == header.KeyId && key.RSA.VerifyHash(
149+
hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
150+
if (verified)
151+
{
152+
var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
153+
154+
// Remove standard claims, so that only custom claims would remain.
155+
foreach (var claim in this.standardClaims)
156+
{
157+
allClaims.Remove(claim);
158+
}
159+
160+
payload.Claims = allClaims.ToImmutableDictionary();
161+
return Task.FromResult(new FirebaseToken(payload));
162+
}
163+
164+
return Task.FromResult(new FirebaseToken(payload));
165+
}
166+
}
167+
}

FirebaseAdmin/FirebaseAdmin/Key.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
3+
namespace FirebaseAdmin
4+
{ /// <summary>
5+
/// Represents a cryptographic key.
6+
/// </summary>
7+
public class Key
8+
{
9+
/// <summary>
10+
/// Gets or sets the key type.
11+
/// </summary>
12+
public string Kty { get; set; }
13+
14+
/// <summary>
15+
/// Gets or sets the intended use of the key.
16+
/// </summary>
17+
public string Use { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the algorithm associated with the key.
21+
/// </summary>
22+
public string Alg { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the key ID.
26+
/// </summary>
27+
public string Kid { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the modulus for the RSA public key.
31+
/// </summary>
32+
public string N { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the exponent for the RSA public key.
36+
/// </summary>
37+
public string E { get; set; }
38+
}
39+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace FirebaseAdmin
5+
{
6+
/// <summary>
7+
/// Represents a cryptographic key.
8+
/// </summary>
9+
public class KeysRoot
10+
{
11+
/// <summary>
12+
/// Gets or sets represents a cryptographic key.
13+
/// </summary>
14+
public List<Key> Keys { get; set; }
15+
}
16+
}

0 commit comments

Comments
 (0)