From 8aa2ee6a12b85aae3a27e06fb88f31b3628e5c7a Mon Sep 17 00:00:00 2001
From: pavlo <taeul930217@skiff.com>
Date: Tue, 16 Jan 2024 02:21:03 -0500
Subject: [PATCH 1/6] migrate appcheck

---
 .../FirebaseAdmin/FirebaseAppCheck.cs         | 167 ++++++++++++++++++
 FirebaseAdmin/FirebaseAdmin/Key.cs            |  39 ++++
 FirebaseAdmin/FirebaseAdmin/KeysRoot.cs       |  16 ++
 3 files changed, 222 insertions(+)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Key.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/KeysRoot.cs

diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
new file mode 100644
index 00000000..764429a2
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth;
+using FirebaseAdmin.Auth.Jwt;
+using Google.Apis.Auth;
+using Newtonsoft.Json;
+using RSAKey = System.Security.Cryptography.RSA;
+
+namespace FirebaseAdmin
+{
+    internal class FirebaseAppCheck
+    {
+        private readonly string appCheckIssuer = "https://firebaseappcheck.googleapis.com/";
+        private readonly string jwksUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";
+        private Dictionary<string, FirebaseToken> appCheck = new Dictionary<string, FirebaseToken>();
+        private string projectId;
+        private string scopedProjectId;
+        private List<Auth.Jwt.PublicKey> cachedKeys;
+        private IReadOnlyList<string> standardClaims =
+            ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
+
+        private FirebaseAppCheck(FirebaseApp app)
+        {
+            this.scopedProjectId = "projects/" + this.projectId;
+            FirebaseTokenVerifier tokenVerifier = FirebaseTokenVerifier.CreateIdTokenVerifier(app);
+            this.projectId = tokenVerifier.ProjectId;
+        }
+
+        public static async Task<FirebaseAppCheck> CreateAsync(FirebaseApp app)
+        {
+            FirebaseAppCheck appCheck = new (app);
+            bool result = await appCheck.Init().ConfigureAwait(false); // If Init fails, handle it accordingly
+            if (!result)
+            {
+                return appCheck;
+                throw new ArgumentException("Error App check initilaization ");
+            }
+
+            return appCheck;
+        }
+
+        public async Task<bool> Init()
+        {
+            try
+            {
+                using var client = new HttpClient();
+                HttpResponseMessage response = await client.GetAsync(this.jwksUrl).ConfigureAwait(false);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+                    KeysRoot keysRoot = JsonConvert.DeserializeObject<KeysRoot>(responseString);
+                    foreach (Key key in keysRoot.Keys)
+                    {
+                        var x509cert = new X509Certificate2(Encoding.UTF8.GetBytes(key.N));
+                        RSAKey rsa = x509cert.GetRSAPublicKey();
+                        this.cachedKeys.Add(new Auth.Jwt.PublicKey(key.Kid, rsa));
+                    }
+
+                    this.cachedKeys.ToImmutableList();
+                    return true;
+                }
+                else
+                {
+                    throw new ArgumentException("Error Http request JwksUrl");
+                }
+            }
+            catch (Exception exception)
+            {
+                throw new ArgumentException("Error Http request", exception);
+            }
+        }
+
+        public async Task<Dictionary<string, FirebaseToken>> VerifyTokenAsync(string token)
+        {
+            if (string.IsNullOrEmpty(token))
+            {
+                throw new ArgumentException("App check token " + token + " must be a non - empty string.");
+            }
+
+            try
+            {
+                FirebaseToken verified_claims = await this.Decode_and_verify(token).ConfigureAwait(false);
+                Dictionary<string, FirebaseToken> appchecks = new ();
+                appchecks.Add(this.projectId, verified_claims);
+                return appchecks;
+            }
+            catch (Exception exception)
+            {
+                throw new ArgumentException("Verifying App Check token failed. Error:", exception);
+            }
+        }
+
+        private Task<FirebaseToken> Decode_and_verify(string token)
+        {
+            string[] segments = token.Split('.');
+            if (segments.Length != 3)
+            {
+                throw new ArgumentException("Incorrect number of segments in Token");
+            }
+
+            var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
+            var payload = JwtUtils.Decode<FirebaseToken.Args>(segments[1]);
+            var projectIdMessage = $"Make sure the comes from the same Firebase "
+                + "project as the credential used to initialize this SDK.";
+            string issuer = this.appCheckIssuer + this.projectId;
+            string error = null;
+            if (header.Algorithm != "RS256")
+            {
+                error = "The provided App Check token has incorrect algorithm. Expected RS256 but got '"
+                    + header.Algorithm + "'";
+            }
+            else if (payload.Audience.Contains(this.scopedProjectId))
+            {
+                error = "The provided App Check token has incorrect 'aud' (audience) claim.Expected "
+                    + $"{this.scopedProjectId} but got {payload.Audience}. {projectIdMessage} ";
+            }
+            else if (!(payload.Issuer is not null) || !payload.Issuer.StartsWith(this.appCheckIssuer))
+            {
+                error = "The provided App Check token has incorrect 'iss' (issuer) claim.";
+            }
+            else if (string.IsNullOrEmpty(payload.Subject))
+            {
+                error = $"Firebase has no or empty subject (sub) claim.";
+            }
+
+            if (error != null)
+            {
+                throw new ArgumentException("invalid - argument" + error);
+            }
+
+            byte[] hash;
+            using (var hashAlg = SHA256.Create())
+            {
+                hash = hashAlg.ComputeHash(
+                    Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
+            }
+
+            var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
+            var verified = this.cachedKeys.Any(key =>
+                key.Id == header.KeyId && key.RSA.VerifyHash(
+                    hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
+            if (verified)
+            {
+                var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
+
+                // Remove standard claims, so that only custom claims would remain.
+                foreach (var claim in this.standardClaims)
+                {
+                    allClaims.Remove(claim);
+                }
+
+                payload.Claims = allClaims.ToImmutableDictionary();
+                return Task.FromResult(new FirebaseToken(payload));
+            }
+
+            return Task.FromResult(new FirebaseToken(payload));
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Key.cs b/FirebaseAdmin/FirebaseAdmin/Key.cs
new file mode 100644
index 00000000..e664acc0
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Key.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace FirebaseAdmin
+{ /// <summary>
+  /// Represents a cryptographic key.
+  /// </summary>
+    public class Key
+    {
+        /// <summary>
+        /// Gets or sets the key type.
+        /// </summary>
+        public string Kty { get; set; }
+
+        /// <summary>
+        /// Gets or sets the intended use of the key.
+        /// </summary>
+        public string Use { get; set; }
+
+        /// <summary>
+        /// Gets or sets the algorithm associated with the key.
+        /// </summary>
+        public string Alg { get; set; }
+
+        /// <summary>
+        /// Gets or sets the key ID.
+        /// </summary>
+        public string Kid { get; set; }
+
+        /// <summary>
+        /// Gets or sets the modulus for the RSA public key.
+        /// </summary>
+        public string N { get; set; }
+
+        /// <summary>
+        /// Gets or sets the exponent for the RSA public key.
+        /// </summary>
+        public string E { get; set; }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/KeysRoot.cs b/FirebaseAdmin/FirebaseAdmin/KeysRoot.cs
new file mode 100644
index 00000000..82a44c15
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/KeysRoot.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace FirebaseAdmin
+{
+    /// <summary>
+    /// Represents a cryptographic key.
+    /// </summary>
+    public class KeysRoot
+    {
+        /// <summary>
+        /// Gets or sets represents a cryptographic key.
+        /// </summary>
+        public List<Key> Keys { get; set; }
+    }
+}

From 7760b874055b72d23c15fc5281aaeb94c9a6a7be Mon Sep 17 00:00:00 2001
From: pavlo <taeul930217@skiff.com>
Date: Fri, 19 Jan 2024 03:20:03 -0500
Subject: [PATCH 2/6] feature/AppcheckTest

---
 .../FirebaseAppCheckTests.cs                  |  76 +++++++++
 .../FirebaseAdmin.Tests/resources/appid.txt   |   1 +
 .../FirebaseAdmin/Auth/FirebaseToken.cs       |   6 +
 .../FirebaseAdmin/Check/AppCheckApiClient.cs  | 107 ++++++++++++
 .../FirebaseAdmin/Check/AppCheckService.cs    |  50 ++++++
 .../FirebaseAdmin/Check/AppCheckToken.cs      |  27 +++
 .../Check/AppCheckTokenGernerator.cs          |  90 ++++++++++
 .../Check/AppCheckTokenOptions.cs             |  22 +++
 .../FirebaseAdmin/Check/CyptoSigner.cs        |  71 ++++++++
 .../FirebaseAdmin/FirebaseAppCheck.cs         | 158 ++++++++++++------
 10 files changed, 553 insertions(+), 55 deletions(-)
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/resources/appid.txt
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckToken.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs

diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
new file mode 100644
index 00000000..685bc334
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
@@ -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();
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/resources/appid.txt b/FirebaseAdmin/FirebaseAdmin.Tests/resources/appid.txt
new file mode 100644
index 00000000..233eb2b8
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/resources/appid.txt
@@ -0,0 +1 @@
+1234,project,Appcheck
\ No newline at end of file
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
index 101ea38c..535d3451 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+using System;
 using System.Collections.Generic;
 using Newtonsoft.Json;
 
@@ -80,6 +81,11 @@ internal FirebaseToken(Args args)
         /// </summary>
         public IReadOnlyDictionary<string, object> Claims { get; }
 
+        public static implicit operator string(FirebaseToken v)
+        {
+            throw new NotImplementedException();
+        }
+
         internal sealed class Args
         {
             [JsonProperty("iss")]
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
new file mode 100644
index 00000000..6c54e3b7
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
@@ -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;
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs
new file mode 100644
index 00000000..27159b05
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs
@@ -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();
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckToken.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckToken.cs
new file mode 100644
index 00000000..1441631c
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckToken.cs
@@ -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;
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
new file mode 100644
index 00000000..25069996
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
@@ -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('/', '_');
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs
new file mode 100644
index 00000000..00e61daa
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// Interface 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)
+    {
+        /// <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;
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs b/FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs
new file mode 100644
index 00000000..d04fd544
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Google.Apis.Auth.OAuth2;
+using Newtonsoft.Json.Linq;
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// Interface representing App Check token options.
+    /// </summary>
+    public class CyptoSigner
+    {
+        private readonly RSA Rsa;
+        private readonly ServiceAccountCredential credential;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CyptoSigner"/> class.
+        /// Interface representing App Check token options.
+        /// </summary>
+        public CyptoSigner(string privateKeyPem)
+        {
+            if (privateKeyPem is null)
+            {
+                throw new ArgumentNullException(nameof(privateKeyPem));
+            }
+
+            this.Rsa = RSA.Create();
+            this.Rsa.ImportFromPem(privateKeyPem.ToCharArray());
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CyptoSigner"/> class.
+        /// Cryptographically signs a buffer of data.
+        /// </summary>
+        /// <param name="buffer">To sign data.</param>
+        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+        public Task<byte[]> Sign(byte[] buffer)
+        {
+            if (buffer is null)
+            {
+                throw new ArgumentNullException(nameof(buffer));
+            }
+
+            // Sign the buffer using the private key and SHA256 hashing algorithm
+            var signature = this.privateKey.SignData(buffer, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+            return Task.FromResult(signature);
+        }
+
+        internal async Task<string> GetAccountId()
+        {
+            var request = new HttpRequestMessage()
+            {
+                Method = HttpMethod.Get,
+                RequestUri = new Uri("http://metadata/computeMetadata/v1/instance/service-accounts/default/email"),
+            };
+            request.Headers.Add("Metadata-Flavor", "Google");
+            var httpClient = new HttpClient();
+            var response = await httpClient.SendAsync(request).ConfigureAwait(false);
+            if (!response.IsSuccessStatusCode)
+            {
+                throw new FirebaseAppCheckException("Error exchanging token.");
+            }
+
+            return response.Content.ToString();
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
index 764429a2..30642e51 100644
--- a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
+++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
@@ -10,49 +10,96 @@
 using System.Threading.Tasks;
 using FirebaseAdmin.Auth;
 using FirebaseAdmin.Auth.Jwt;
+using FirebaseAdmin.Check;
 using Google.Apis.Auth;
 using Newtonsoft.Json;
 using RSAKey = System.Security.Cryptography.RSA;
 
 namespace FirebaseAdmin
 {
-    internal class FirebaseAppCheck
+    /// <summary>
+    /// Asynchronously creates a new Firebase App Check token for the specified Firebase app.
+    /// </summary>
+    /// <returns>A task that completes with the creation of a new App Check token.</returns>
+    /// <exception cref="FirebaseAppCheckException">Thrown if an error occurs while creating the custom token.</exception>
+    public sealed class FirebaseAppCheck
     {
-        private readonly string appCheckIssuer = "https://firebaseappcheck.googleapis.com/";
-        private readonly string jwksUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";
-        private Dictionary<string, FirebaseToken> appCheck = new Dictionary<string, FirebaseToken>();
-        private string projectId;
-        private string scopedProjectId;
-        private List<Auth.Jwt.PublicKey> cachedKeys;
-        private IReadOnlyList<string> standardClaims =
+        private static readonly string AppCheckIssuer = "https://firebaseappcheck.googleapis.com/";
+        private static readonly string ProjectId;
+        private static readonly string ScopedProjectId;
+        private static readonly string JwksUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";
+        private static readonly IReadOnlyList<string> StandardClaims =
             ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
 
-        private FirebaseAppCheck(FirebaseApp app)
+        private static List<Auth.Jwt.PublicKey> cachedKeys;
+
+        /// <summary>
+        /// Creates a new {@link AppCheckToken} that can be sent back to a client.
+        /// </summary>
+        /// <param name="appId">ID of Firebase App.</param>
+        /// <param name="options">Options of FirebaseApp.</param>
+        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
+        public static async Task<AppCheckToken> CreateToken(string appId, AppCheckTokenOptions options = null)
         {
-            this.scopedProjectId = "projects/" + this.projectId;
-            FirebaseTokenVerifier tokenVerifier = FirebaseTokenVerifier.CreateIdTokenVerifier(app);
-            this.projectId = tokenVerifier.ProjectId;
+            if (string.IsNullOrEmpty(appId))
+            {
+                throw new ArgumentNullException("AppId must be a non-empty string.");
+            }
+
+            if (options == null)
+            {
+                var customOptions = AppCheckService.ValidateTokenOptions(options);
+            }
+
+            string customToken = " ";
+            try
+            {
+                customToken = AppCheckTokenGernerator.CreateCustomToken(appId, options);
+            }
+            catch (Exception e)
+            {
+                throw new FirebaseAppCheckException("Error Create customToken", e.Message);
+            }
+
+            AppCheckApiClient appCheckApiClient = new AppCheckApiClient(appId);
+            return await appCheckApiClient.ExchangeToken(customToken).ConfigureAwait(false);
         }
 
-        public static async Task<FirebaseAppCheck> CreateAsync(FirebaseApp app)
+        /// <summary>
+        /// Verifies the format and signature of a Firebase App Check token.
+        /// </summary>
+        /// <param name="token"> The Firebase Auth JWT token to verify.</param>
+        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
+        public static async Task<Dictionary<string, FirebaseToken>> VerifyTokenAsync(string token)
         {
-            FirebaseAppCheck appCheck = new (app);
-            bool result = await appCheck.Init().ConfigureAwait(false); // If Init fails, handle it accordingly
-            if (!result)
+            if (string.IsNullOrEmpty(token))
             {
-                return appCheck;
-                throw new ArgumentException("Error App check initilaization ");
+                throw new ArgumentNullException("App check token " + token + " must be a non - empty string.");
             }
 
-            return appCheck;
+            try
+            {
+                FirebaseToken verified_claims = await Decode_and_verify(token).ConfigureAwait(false);
+                Dictionary<string, FirebaseToken> appchecks = new ();
+                appchecks.Add(ProjectId, verified_claims);
+                return appchecks;
+            }
+            catch (Exception exception)
+            {
+                throw new ArgumentNullException("Verifying App Check token failed. Error:", exception);
+            }
         }
 
-        public async Task<bool> Init()
+        /// <summary>
+        /// Get public key from jwksUrl.
+        /// </summary>
+        /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
+        public static async Task InitializeAsync()
         {
             try
             {
                 using var client = new HttpClient();
-                HttpResponseMessage response = await client.GetAsync(this.jwksUrl).ConfigureAwait(false);
+                HttpResponseMessage response = await client.GetAsync(JwksUrl).ConfigureAwait(false);
                 if (response.StatusCode == HttpStatusCode.OK)
                 {
                     string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
@@ -61,68 +108,52 @@ public async Task<bool> Init()
                     {
                         var x509cert = new X509Certificate2(Encoding.UTF8.GetBytes(key.N));
                         RSAKey rsa = x509cert.GetRSAPublicKey();
-                        this.cachedKeys.Add(new Auth.Jwt.PublicKey(key.Kid, rsa));
+                        cachedKeys.Add(new Auth.Jwt.PublicKey(key.Kid, rsa));
                     }
 
-                    this.cachedKeys.ToImmutableList();
-                    return true;
+                    cachedKeys.ToImmutableList();
                 }
                 else
                 {
-                    throw new ArgumentException("Error Http request JwksUrl");
+                    throw new ArgumentNullException("Error Http request JwksUrl");
                 }
             }
             catch (Exception exception)
             {
-                throw new ArgumentException("Error Http request", exception);
+                throw new ArgumentNullException("Error Http request", exception);
             }
         }
 
-        public async Task<Dictionary<string, FirebaseToken>> VerifyTokenAsync(string token)
-        {
-            if (string.IsNullOrEmpty(token))
-            {
-                throw new ArgumentException("App check token " + token + " must be a non - empty string.");
-            }
-
-            try
-            {
-                FirebaseToken verified_claims = await this.Decode_and_verify(token).ConfigureAwait(false);
-                Dictionary<string, FirebaseToken> appchecks = new ();
-                appchecks.Add(this.projectId, verified_claims);
-                return appchecks;
-            }
-            catch (Exception exception)
-            {
-                throw new ArgumentException("Verifying App Check token failed. Error:", exception);
-            }
-        }
-
-        private Task<FirebaseToken> Decode_and_verify(string token)
+        /// <summary>
+        /// Decode_and_verify.
+        /// </summary>
+        /// <param name="token">The Firebase Auth JWT token to verify.</param>
+        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
+        public static Task<FirebaseToken> Decode_and_verify(string token)
         {
             string[] segments = token.Split('.');
             if (segments.Length != 3)
             {
-                throw new ArgumentException("Incorrect number of segments in Token");
+                throw new FirebaseAppCheckException("Incorrect number of segments in Token");
             }
 
             var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
             var payload = JwtUtils.Decode<FirebaseToken.Args>(segments[1]);
             var projectIdMessage = $"Make sure the comes from the same Firebase "
                 + "project as the credential used to initialize this SDK.";
-            string issuer = this.appCheckIssuer + this.projectId;
+            string issuer = AppCheckIssuer + ProjectId;
             string error = null;
             if (header.Algorithm != "RS256")
             {
                 error = "The provided App Check token has incorrect algorithm. Expected RS256 but got '"
                     + header.Algorithm + "'";
             }
-            else if (payload.Audience.Contains(this.scopedProjectId))
+            else if (payload.Audience.Contains(ScopedProjectId))
             {
                 error = "The provided App Check token has incorrect 'aud' (audience) claim.Expected "
-                    + $"{this.scopedProjectId} but got {payload.Audience}. {projectIdMessage} ";
+                    + $"{ScopedProjectId} but got {payload.Audience}. {projectIdMessage} ";
             }
-            else if (!(payload.Issuer is not null) || !payload.Issuer.StartsWith(this.appCheckIssuer))
+            else if (!(payload.Issuer is not null) || !payload.Issuer.StartsWith(AppCheckIssuer))
             {
                 error = "The provided App Check token has incorrect 'iss' (issuer) claim.";
             }
@@ -133,7 +164,7 @@ private Task<FirebaseToken> Decode_and_verify(string token)
 
             if (error != null)
             {
-                throw new ArgumentException("invalid - argument" + error);
+                throw new InvalidOperationException("invalid - argument" + error);
             }
 
             byte[] hash;
@@ -144,7 +175,7 @@ private Task<FirebaseToken> Decode_and_verify(string token)
             }
 
             var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
-            var verified = this.cachedKeys.Any(key =>
+            var verified = cachedKeys.Any(key =>
                 key.Id == header.KeyId && key.RSA.VerifyHash(
                     hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
             if (verified)
@@ -152,7 +183,7 @@ private Task<FirebaseToken> Decode_and_verify(string token)
                 var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
 
                 // Remove standard claims, so that only custom claims would remain.
-                foreach (var claim in this.standardClaims)
+                foreach (var claim in StandardClaims)
                 {
                     allClaims.Remove(claim);
                 }
@@ -163,5 +194,22 @@ private Task<FirebaseToken> Decode_and_verify(string token)
 
             return Task.FromResult(new FirebaseToken(payload));
         }
+
+        /// <summary>
+        /// Deleted all the apps created so far. Used for unit testing.
+        /// </summary>
+        public static void Delete()
+        {
+            lock (cachedKeys)
+            {
+                var copy = new List<Auth.Jwt.PublicKey>(cachedKeys);
+                copy.Clear();
+
+                if (cachedKeys.Count > 0)
+                {
+                    throw new InvalidOperationException("Failed to delete all apps");
+                }
+            }
+        }
     }
 }

From bc8360f4eb4a32bfacc61e296a8fd6a61a5dbb08 Mon Sep 17 00:00:00 2001
From: pavlo <taeul930217@skiff.com>
Date: Tue, 23 Jan 2024 04:21:31 -0500
Subject: [PATCH 3/6] feature/app-check/test

---
 .../FirebaseAppCheckTests.cs                  | 111 ++++++---
 .../FirebaseAdmin/Auth/FirebaseToken.cs       |  14 +-
 .../Jwt}/AppCheckTokenOptions.cs              |  10 +-
 .../Auth/Jwt/FirebaseTokenFactory.cs          |  66 ++++-
 .../Auth/Jwt/FirebaseTokenVerifier.cs         |  40 ++++
 .../Auth/Jwt/HttpPublicKeySource.cs           |  45 +++-
 .../FirebaseAdmin/Check/AppCheckApiClient.cs  | 141 +++++++++--
 .../FirebaseAdmin/Check/AppCheckService.cs    |  50 ----
 .../Check/AppCheckTokenGernerator.cs          |  90 -------
 .../Check/AppCheckVerifyResponse.cs           |  28 +++
 .../FirebaseAdmin/Check/CyptoSigner.cs        |  71 ------
 .../FirebaseAdmin/Check/IAppCheckApiClient.cs |  37 +++
 .../FirebaseAdmin/{ => Check}/Key.cs          |   0
 .../FirebaseAdmin/{ => Check}/KeysRoot.cs     |   2 +-
 .../Check/VerifyAppCheckTokenOptions.cs       |  27 +++
 .../FirebaseAdmin/FirebaseAppCheck.cs         | 225 ++++++------------
 16 files changed, 525 insertions(+), 432 deletions(-)
 rename FirebaseAdmin/FirebaseAdmin/{Check => Auth/Jwt}/AppCheckTokenOptions.cs (67%)
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs
 rename FirebaseAdmin/FirebaseAdmin/{ => Check}/Key.cs (100%)
 rename FirebaseAdmin/FirebaseAdmin/{ => Check}/KeysRoot.cs (91%)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Check/VerifyAppCheckTokenOptions.cs

diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
index 685bc334..53814bd9 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
@@ -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();
         }
     }
 }
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
index 535d3451..e9ae348a 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
@@ -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;
@@ -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.
@@ -81,6 +87,10 @@ 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();
@@ -88,7 +98,9 @@ public static implicit operator string(FirebaseToken v)
 
         internal sealed class Args
         {
-            [JsonProperty("iss")]
+            public string AppId { get; internal set; }
+
+            [JsonProperty("app_id")]
             internal string Issuer { get; set; }
 
             [JsonProperty("sub")]
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/AppCheckTokenOptions.cs
similarity index 67%
rename from FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs
rename to FirebaseAdmin/FirebaseAdmin/Auth/Jwt/AppCheckTokenOptions.cs
index 00e61daa..453f9c5e 100644
--- a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenOptions.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/AppCheckTokenOptions.cs
@@ -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;
     }
 }
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
index 8d7dcac0..d3343213 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
@@ -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);
 
@@ -58,7 +60,8 @@ internal class FirebaseTokenFactory : IDisposable
             "jti",
             "nbf",
             "nonce",
-            "sub");
+            "sub",
+            "app_id");
 
         internal FirebaseTokenFactory(Args args)
         {
@@ -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; }
         }
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
index 50fad5f7..1d37c094 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
@@ -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";
 
@@ -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))
         {
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs
index b7786c94..e8f8a303 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs
@@ -15,14 +15,18 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.Immutable;
+using System.Net;
 using System.Net.Http;
 using System.Security.Cryptography.X509Certificates;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using FirebaseAdmin.Check;
 using FirebaseAdmin.Util;
 using Google.Apis.Http;
 using Google.Apis.Util;
+using Newtonsoft.Json;
+using static Google.Apis.Requests.BatchRequest;
 using RSAKey = System.Security.Cryptography.RSA;
 
 namespace FirebaseAdmin.Auth.Jwt
@@ -74,11 +78,19 @@ public async Task<IReadOnlyList<PublicKey>> GetPublicKeysAsync(
                                 Method = HttpMethod.Get,
                                 RequestUri = new Uri(this.certUrl),
                             };
+
                             var response = await httpClient
                                 .SendAndDeserializeAsync<Dictionary<string, string>>(request, cancellationToken)
                                 .ConfigureAwait(false);
+                            if (this.certUrl != "https://firebaseappcheck.googleapis.com/v1/jwks")
+                            {
+                                this.cachedKeys = this.ParseKeys(response);
+                            }
+                            else
+                            {
+                                this.cachedKeys = await this.ParseAppCheckKeys().ConfigureAwait(false);
+                            }
 
-                            this.cachedKeys = this.ParseKeys(response);
                             var cacheControl = response.HttpResponse.Headers.CacheControl;
                             if (cacheControl?.MaxAge != null)
                             {
@@ -132,6 +144,37 @@ private IReadOnlyList<PublicKey> ParseKeys(DeserializedResponseInfo<Dictionary<s
             return builder.ToImmutableList();
         }
 
+        private async Task<IReadOnlyList<PublicKey>> ParseAppCheckKeys()
+        {
+            try
+            {
+                using var client = new HttpClient();
+                HttpResponseMessage response = await client.GetAsync(this.certUrl).ConfigureAwait(false);
+                if (response.StatusCode == HttpStatusCode.OK)
+                {
+                    string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+                    KeysRoot keysRoot = JsonConvert.DeserializeObject<KeysRoot>(responseString);
+                    var builder = ImmutableList.CreateBuilder<PublicKey>();
+                    foreach (Key key in keysRoot.Keys)
+                    {
+                        var x509cert = new X509Certificate2(Encoding.UTF8.GetBytes(key.N));
+                        RSAKey rsa = x509cert.GetRSAPublicKey();
+                        builder.Add(new PublicKey(key.Kid, rsa));
+                    }
+
+                    return builder.ToImmutableList();
+                }
+                else
+                {
+                    throw new ArgumentNullException("Error Http request JwksUrl");
+                }
+            }
+            catch (Exception exception)
+            {
+                throw new ArgumentNullException("Error Http request", exception);
+            }
+        }
+
         private class HttpKeySourceErrorHandler
         : HttpErrorHandler<FirebaseAuthException>,
             IHttpRequestExceptionHandler<FirebaseAuthException>,
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
index 6c54e3b7..fa909b55 100644
--- a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
@@ -6,17 +6,30 @@
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using FirebaseAdmin.Auth;
+using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
+using static Google.Apis.Requests.BatchRequest;
 
 namespace FirebaseAdmin.Check
 {
+    /// <summary>
+    /// Class that facilitates sending requests to the Firebase App Check backend API.
+    /// </summary>
+    /// <returns>A task that completes with the creation of a new App Check token.</returns>
+    /// <exception cref="ArgumentNullException">Thrown if an error occurs while creating the custom token.</exception>
+    /// <value>The Firebase app instance.</value>
     internal class AppCheckApiClient
     {
         private const string ApiUrlFormat = "https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken";
+        private const string OneTimeUseTokenVerificationUrlFormat = "https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}:verifyAppCheckToken";
         private readonly FirebaseApp app;
         private string projectId;
-        private string appId;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AppCheckApiClient"/> class.
+        /// </summary>
+        /// <param name="value"> Initailize FirebaseApp. </param>
         public AppCheckApiClient(FirebaseApp value)
         {
             if (value == null || value.Options == null)
@@ -28,45 +41,115 @@ public AppCheckApiClient(FirebaseApp value)
             this.projectId = this.app.Options.ProjectId;
         }
 
-        public AppCheckApiClient(string appId)
+        /// <summary>
+        /// Exchange a signed custom token to App Check token.
+        /// </summary>
+        /// <param name="customToken"> The custom token to be exchanged. </param>
+        /// <param name="appId"> The mobile App ID.</param>
+        /// <returns>A <see cref="Task{AppCheckToken}"/> A promise that fulfills with a `AppCheckToken`.</returns>
+        public async Task<AppCheckToken> ExchangeTokenAsync(string customToken, string appId)
         {
-            this.appId = appId;
-        }
-
-        public async Task<AppCheckToken> ExchangeToken(string customToken)
-        {
-            if (customToken == null)
+            if (string.IsNullOrEmpty(customToken))
             {
                 throw new ArgumentException("First argument passed to customToken must be a valid Firebase app instance.");
             }
 
-            if (this.appId == null)
+            if (string.IsNullOrEmpty(appId))
             {
                 throw new ArgumentException("Second argument passed to appId must be a valid Firebase app instance.");
             }
 
-            var url = this.GetUrl(this.appId);
+            HttpResponseMessage response = await this.GetExchangeToken(customToken, appId).ConfigureAwait(false);
+
+            JObject responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
+            string tokenValue = responseData["data"]["token"].ToString();
+            int ttlValue = this.StringToMilliseconds(responseData["data"]["ttl"].ToString());
+            AppCheckToken appCheckToken = new (tokenValue, ttlValue);
+            return appCheckToken;
+        }
+
+        /// <summary>
+        /// Exchange a signed custom token to App Check token.
+        /// </summary>
+        /// <param name="customToken"> The custom token to be exchanged. </param>
+        /// <param name="appId"> The Id of Firebase App. </param>
+        /// <returns> HttpResponseMessage .</returns>
+        public async Task<HttpResponseMessage> GetExchangeToken(string customToken, string appId)
+        {
+            var url = this.GetUrl(appId);
+            var content = new StringContent(JsonConvert.SerializeObject(new { customToken }), Encoding.UTF8, "application/json");
             var request = new HttpRequestMessage()
             {
                 Method = HttpMethod.Post,
                 RequestUri = new Uri(url),
-                Content = new StringContent(customToken),
+                Content = content,
             };
-            request.Headers.Add("X-Firebase-Client", "fire-admin-node/${utils.getSdkVersion()}");
+            request.Headers.Add("X-Firebase-Client", "fire-admin-node/" + $"{FirebaseApp.GetSdkVersion()}");
+            Console.WriteLine(request.Content);
             var httpClient = new HttpClient();
             var response = await httpClient.SendAsync(request).ConfigureAwait(false);
-            if (!response.IsSuccessStatusCode)
+            if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
+            {
+                var errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
+                throw new InvalidOperationException($"BadRequest: {errorContent}");
+            }
+            else if (!response.IsSuccessStatusCode)
             {
-                throw new ArgumentException("Error exchanging token.");
+                throw new ArgumentException("unknown-error", $"Unexpected response with status:{response.StatusCode}");
             }
 
+            return response;
+        }
+
+        /// <summary>
+        /// Exchange a signed custom token to App Check token.
+        /// </summary>
+        /// <param name="token"> The custom token to be exchanged. </param>
+        /// <returns>A alreadyConsumed is true.</returns>
+        public async Task<bool> VerifyReplayProtection(string token)
+        {
+            if (string.IsNullOrEmpty(token))
+            {
+                throw new ArgumentException("invalid-argument", "`token` must be a non-empty string.");
+            }
+
+            string url = this.GetVerifyTokenUrl();
+
+            var request = new HttpRequestMessage()
+            {
+                Method = HttpMethod.Post,
+                RequestUri = new Uri(url),
+                Content = new StringContent(token),
+            };
+
+            var httpClient = new HttpClient();
+            var response = await httpClient.SendAsync(request).ConfigureAwait(false);
+
             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;
+            bool alreadyConsumed = (bool)responseData["data"]["alreadyConsumed"];
+            return alreadyConsumed;
+          }
+
+        /// <summary>
+        /// Get Verify Token Url .
+        /// </summary>
+        /// <returns>A formatted verify token url.</returns>
+        private string GetVerifyTokenUrl()
+        {
+            var urlParams = new Dictionary<string, string>
+            {
+                { "projectId", this.projectId },
+            };
+
+            string baseUrl = this.FormatString(OneTimeUseTokenVerificationUrlFormat, urlParams);
+            return this.FormatString(baseUrl, null);
         }
 
+        /// <summary>
+        /// Get url from FirebaseApp Id .
+        /// </summary>
+        /// <param name="appId">The FirebaseApp Id.</param>
+        /// <returns>A formatted verify token url.</returns>
         private string GetUrl(string appId)
         {
             if (string.IsNullOrEmpty(this.projectId))
@@ -93,6 +176,28 @@ private string GetUrl(string appId)
             return baseUrl;
         }
 
+        /// <summary>
+        /// Converts a duration string with the suffix `s` to milliseconds.
+        /// </summary>
+        /// <param name="duration">The duration as a string with the suffix "s" preceded by the number of seconds.</param>
+        /// <returns> The duration in milliseconds.</returns>
+        private int StringToMilliseconds(string duration)
+        {
+            if (string.IsNullOrEmpty(duration) || !duration.EndsWith("s"))
+            {
+                throw new ArgumentException("invalid-argument", "`ttl` must be a valid duration string with the suffix `s`.");
+            }
+
+            string modifiedString = duration.Remove(duration.Length - 1);
+            return int.Parse(modifiedString) * 1000;
+        }
+
+        /// <summary>
+        /// Formats a string of form 'project/{projectId}/{api}' and replaces with corresponding arguments {projectId: '1234', api: 'resource'}.
+        /// </summary>
+        /// <param name="str">The original string where the param need to be replaced.</param>
+        /// <param name="urlParams">The optional parameters to replace in thestring.</param>
+        /// <returns> The resulting formatted string. </returns>
         private string FormatString(string str, Dictionary<string, string> urlParams)
         {
             string formatted = str;
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs
deleted file mode 100644
index 27159b05..00000000
--- a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckService.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-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();
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
deleted file mode 100644
index 25069996..00000000
--- a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckTokenGernerator.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-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('/', '_');
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs
new file mode 100644
index 00000000..90ae92f4
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using FirebaseAdmin.Auth;
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// AppCheckVerifyResponse.
+    /// </summary>
+    public class AppCheckVerifyResponse(string appId, FirebaseToken verifiedToken, bool alreadyConsumed = false)
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether gets the Firebase App Check token.
+        /// </summary>
+        public bool AlreadyConsumed { get; set; } = alreadyConsumed;
+
+        /// <summary>
+        /// Gets or sets the Firebase App Check token.
+        /// </summary>
+        public string AppId { get; set; } = appId;
+
+        /// <summary>
+        /// Gets or sets the Firebase App Check VerifiedToken.
+        /// </summary>
+        public string VerifiedToken { get; set; } = verifiedToken;
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs b/FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs
deleted file mode 100644
index d04fd544..00000000
--- a/FirebaseAdmin/FirebaseAdmin/Check/CyptoSigner.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading.Tasks;
-using Google.Apis.Auth.OAuth2;
-using Newtonsoft.Json.Linq;
-
-namespace FirebaseAdmin.Check
-{
-    /// <summary>
-    /// Interface representing App Check token options.
-    /// </summary>
-    public class CyptoSigner
-    {
-        private readonly RSA Rsa;
-        private readonly ServiceAccountCredential credential;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CyptoSigner"/> class.
-        /// Interface representing App Check token options.
-        /// </summary>
-        public CyptoSigner(string privateKeyPem)
-        {
-            if (privateKeyPem is null)
-            {
-                throw new ArgumentNullException(nameof(privateKeyPem));
-            }
-
-            this.Rsa = RSA.Create();
-            this.Rsa.ImportFromPem(privateKeyPem.ToCharArray());
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CyptoSigner"/> class.
-        /// Cryptographically signs a buffer of data.
-        /// </summary>
-        /// <param name="buffer">To sign data.</param>
-        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
-        public Task<byte[]> Sign(byte[] buffer)
-        {
-            if (buffer is null)
-            {
-                throw new ArgumentNullException(nameof(buffer));
-            }
-
-            // Sign the buffer using the private key and SHA256 hashing algorithm
-            var signature = this.privateKey.SignData(buffer, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
-            return Task.FromResult(signature);
-        }
-
-        internal async Task<string> GetAccountId()
-        {
-            var request = new HttpRequestMessage()
-            {
-                Method = HttpMethod.Get,
-                RequestUri = new Uri("http://metadata/computeMetadata/v1/instance/service-accounts/default/email"),
-            };
-            request.Headers.Add("Metadata-Flavor", "Google");
-            var httpClient = new HttpClient();
-            var response = await httpClient.SendAsync(request).ConfigureAwait(false);
-            if (!response.IsSuccessStatusCode)
-            {
-                throw new FirebaseAppCheckException("Error exchanging token.");
-            }
-
-            return response.Content.ToString();
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs
new file mode 100644
index 00000000..2e281079
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// Interface of Firebase App Check backend API .
+    /// </summary>
+    internal interface IAppCheckApiClient
+    {
+        /// <summary>
+        /// Exchange a signed custom token to App Check token.
+        /// </summary>
+        /// <param name="customToken">The custom token to be exchanged.</param>
+        /// <param name="appId">The mobile App ID.</param>
+        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
+        public Task<AppCheckToken> ExchangeTokenAsync(string customToken, string appId);
+
+        /// <summary>
+        /// Exchange a signed custom token to App Check token.
+        /// </summary>
+        /// <param name="token"> The custom token to be exchanged. </param>
+        /// <returns>A alreadyConsumed is true.</returns>
+        public Task<bool> VerifyReplayProtection(string token);
+
+        /// <summary>
+        /// Exchange a signed custom token to App Check token.
+        /// </summary>
+        /// <param name="customToken"> The custom token to be exchanged. </param>
+        /// <param name="appId"> The Id of Firebase App. </param>
+        /// <returns> HttpResponseMessage .</returns>
+        public Task<HttpResponseMessage> GetExchangeToken(string customToken, string appId);
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Key.cs b/FirebaseAdmin/FirebaseAdmin/Check/Key.cs
similarity index 100%
rename from FirebaseAdmin/FirebaseAdmin/Key.cs
rename to FirebaseAdmin/FirebaseAdmin/Check/Key.cs
diff --git a/FirebaseAdmin/FirebaseAdmin/KeysRoot.cs b/FirebaseAdmin/FirebaseAdmin/Check/KeysRoot.cs
similarity index 91%
rename from FirebaseAdmin/FirebaseAdmin/KeysRoot.cs
rename to FirebaseAdmin/FirebaseAdmin/Check/KeysRoot.cs
index 82a44c15..38913f1a 100644
--- a/FirebaseAdmin/FirebaseAdmin/KeysRoot.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Check/KeysRoot.cs
@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
 
-namespace FirebaseAdmin
+namespace FirebaseAdmin.Check
 {
     /// <summary>
     /// Represents a cryptographic key.
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/VerifyAppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/Check/VerifyAppCheckTokenOptions.cs
new file mode 100644
index 00000000..000adac5
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Check/VerifyAppCheckTokenOptions.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+/// <summary>
+/// Class representing options for the AppCheck.VerifyToken method.
+/// </summary>
+public class VerifyAppCheckTokenOptions
+{
+    /// <summary>
+    /// Gets or sets a value indicating whether to use the replay protection feature, set this to true. The AppCheck.VerifyToken
+    /// method will mark the token as consumed after verifying it.
+    ///
+    /// Tokens that are found to be already consumed will be marked as such in the response.
+    ///
+    /// Tokens are only considered to be consumed if it is sent to App Check backend by calling the
+    /// AppCheck.VerifyToken method with this field set to true; other uses of the token
+    /// do not consume it.
+    ///
+    /// This replay protection feature requires an additional network call to the App Check backend
+    /// and forces your clients to obtain a fresh attestation from your chosen attestation providers.
+    /// This can therefore negatively impact performance and can potentially deplete your attestation
+    /// providers' quotas faster. We recommend that you use this feature only for protecting
+    /// low volume, security critical, or expensive operations.
+    /// </summary>
+    public bool Consume { get; set; } = false;
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
index 30642e51..0710bfa8 100644
--- a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
+++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
@@ -1,19 +1,11 @@
 using System;
 using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
 using System.Threading.Tasks;
+using System.Xml.Linq;
 using FirebaseAdmin.Auth;
 using FirebaseAdmin.Auth.Jwt;
 using FirebaseAdmin.Check;
-using Google.Apis.Auth;
-using Newtonsoft.Json;
-using RSAKey = System.Security.Cryptography.RSA;
+using Google.Apis.Logging;
 
 namespace FirebaseAdmin
 {
@@ -21,195 +13,114 @@ namespace FirebaseAdmin
     /// Asynchronously creates a new Firebase App Check token for the specified Firebase app.
     /// </summary>
     /// <returns>A task that completes with the creation of a new App Check token.</returns>
-    /// <exception cref="FirebaseAppCheckException">Thrown if an error occurs while creating the custom token.</exception>
+    /// <exception cref="ArgumentNullException">Thrown if an error occurs while creating the custom token.</exception>
+    /// <value>The Firebase app instance.</value>
     public sealed class FirebaseAppCheck
     {
-        private static readonly string AppCheckIssuer = "https://firebaseappcheck.googleapis.com/";
-        private static readonly string ProjectId;
-        private static readonly string ScopedProjectId;
-        private static readonly string JwksUrl = "https://firebaseappcheck.googleapis.com/v1/jwks";
-        private static readonly IReadOnlyList<string> StandardClaims =
-            ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
+        private const string DefaultProjectId = "[DEFAULT]";
+        private static Dictionary<string, FirebaseAppCheck> appChecks = new Dictionary<string, FirebaseAppCheck>();
 
-        private static List<Auth.Jwt.PublicKey> cachedKeys;
+        private readonly AppCheckApiClient apiClient;
+        private readonly FirebaseTokenVerifier appCheckTokenVerifier;
+        private readonly FirebaseTokenFactory tokenFactory;
 
         /// <summary>
-        /// Creates a new {@link AppCheckToken} that can be sent back to a client.
+        /// Initializes a new instance of the <see cref="FirebaseAppCheck"/> class.
         /// </summary>
-        /// <param name="appId">ID of Firebase App.</param>
-        /// <param name="options">Options of FirebaseApp.</param>
-        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
-        public static async Task<AppCheckToken> CreateToken(string appId, AppCheckTokenOptions options = null)
+        /// <param name="value"> Initailize FirebaseApp. </param>
+        public FirebaseAppCheck(FirebaseApp value)
         {
-            if (string.IsNullOrEmpty(appId))
-            {
-                throw new ArgumentNullException("AppId must be a non-empty string.");
-            }
-
-            if (options == null)
-            {
-                var customOptions = AppCheckService.ValidateTokenOptions(options);
-            }
-
-            string customToken = " ";
-            try
-            {
-                customToken = AppCheckTokenGernerator.CreateCustomToken(appId, options);
-            }
-            catch (Exception e)
-            {
-                throw new FirebaseAppCheckException("Error Create customToken", e.Message);
-            }
-
-            AppCheckApiClient appCheckApiClient = new AppCheckApiClient(appId);
-            return await appCheckApiClient.ExchangeToken(customToken).ConfigureAwait(false);
+            this.apiClient = new AppCheckApiClient(value);
+            this.tokenFactory = FirebaseTokenFactory.Create(value);
+            this.appCheckTokenVerifier = FirebaseTokenVerifier.CreateAppCheckVerifier(value);
         }
 
         /// <summary>
-        /// Verifies the format and signature of a Firebase App Check token.
+        /// Initializes a new instance of the <see cref="FirebaseAppCheck"/> class.
         /// </summary>
-        /// <param name="token"> The Firebase Auth JWT token to verify.</param>
-        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
-        public static async Task<Dictionary<string, FirebaseToken>> VerifyTokenAsync(string token)
+        /// <param name="app"> Initailize FirebaseApp. </param>
+        /// <returns>A <see cref="Task{FirebaseAppCheck}"/> Representing the result of the asynchronous operation.</returns>
+        public static FirebaseAppCheck Create(FirebaseApp app)
         {
-            if (string.IsNullOrEmpty(token))
-            {
-                throw new ArgumentNullException("App check token " + token + " must be a non - empty string.");
-            }
+            string appId = app.Name;
 
-            try
+            if (app == null)
             {
-                FirebaseToken verified_claims = await Decode_and_verify(token).ConfigureAwait(false);
-                Dictionary<string, FirebaseToken> appchecks = new ();
-                appchecks.Add(ProjectId, verified_claims);
-                return appchecks;
+                throw new ArgumentNullException("FirebaseApp must not be null or empty");
             }
-            catch (Exception exception)
-            {
-                throw new ArgumentNullException("Verifying App Check token failed. Error:", exception);
-            }
-        }
 
-        /// <summary>
-        /// Get public key from jwksUrl.
-        /// </summary>
-        /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
-        public static async Task InitializeAsync()
-        {
-            try
+            lock (appChecks)
             {
-                using var client = new HttpClient();
-                HttpResponseMessage response = await client.GetAsync(JwksUrl).ConfigureAwait(false);
-                if (response.StatusCode == HttpStatusCode.OK)
+                if (appChecks.ContainsKey(appId))
                 {
-                    string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-                    KeysRoot keysRoot = JsonConvert.DeserializeObject<KeysRoot>(responseString);
-                    foreach (Key key in keysRoot.Keys)
+                    if (appId == DefaultProjectId)
                     {
-                        var x509cert = new X509Certificate2(Encoding.UTF8.GetBytes(key.N));
-                        RSAKey rsa = x509cert.GetRSAPublicKey();
-                        cachedKeys.Add(new Auth.Jwt.PublicKey(key.Kid, rsa));
+                        throw new ArgumentException("The default FirebaseAppCheck already exists.");
+                    }
+                    else
+                    {
+                        throw new ArgumentException($"FirebaseApp named {appId} already exists.");
                     }
-
-                    cachedKeys.ToImmutableList();
-                }
-                else
-                {
-                    throw new ArgumentNullException("Error Http request JwksUrl");
                 }
             }
-            catch (Exception exception)
-            {
-                throw new ArgumentNullException("Error Http request", exception);
-            }
+
+            var appCheck = new FirebaseAppCheck(app);
+            appChecks.Add(appId, appCheck);
+            return appCheck;
         }
 
         /// <summary>
-        /// Decode_and_verify.
+        /// Creates a new AppCheckToken that can be sent back to a client.
         /// </summary>
-        /// <param name="token">The Firebase Auth JWT token to verify.</param>
-        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
-        public static Task<FirebaseToken> Decode_and_verify(string token)
+        /// <param name="appId">The app ID to use as the JWT app_id.</param>
+        /// <param name="options">Optional options object when creating a new App Check Token.</param>
+        /// <returns>A <see cref="Task{AppCheckToken}"/> Representing the result of the asynchronous operation.</returns>
+        public async Task<AppCheckToken> CreateToken(string appId, AppCheckTokenOptions options = null)
         {
-            string[] segments = token.Split('.');
-            if (segments.Length != 3)
-            {
-                throw new FirebaseAppCheckException("Incorrect number of segments in Token");
-            }
+            string customToken = await this.tokenFactory.CreateCustomTokenAppIdAsync(appId, options)
+                .ConfigureAwait(false);
 
-            var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
-            var payload = JwtUtils.Decode<FirebaseToken.Args>(segments[1]);
-            var projectIdMessage = $"Make sure the comes from the same Firebase "
-                + "project as the credential used to initialize this SDK.";
-            string issuer = AppCheckIssuer + ProjectId;
-            string error = null;
-            if (header.Algorithm != "RS256")
-            {
-                error = "The provided App Check token has incorrect algorithm. Expected RS256 but got '"
-                    + header.Algorithm + "'";
-            }
-            else if (payload.Audience.Contains(ScopedProjectId))
-            {
-                error = "The provided App Check token has incorrect 'aud' (audience) claim.Expected "
-                    + $"{ScopedProjectId} but got {payload.Audience}. {projectIdMessage} ";
-            }
-            else if (!(payload.Issuer is not null) || !payload.Issuer.StartsWith(AppCheckIssuer))
-            {
-                error = "The provided App Check token has incorrect 'iss' (issuer) claim.";
-            }
-            else if (string.IsNullOrEmpty(payload.Subject))
-            {
-                error = $"Firebase has no or empty subject (sub) claim.";
-            }
+            return await this.apiClient.ExchangeTokenAsync(customToken, appId).ConfigureAwait(false);
+        }
 
-            if (error != null)
+        /// <summary>
+        /// Verifies a Firebase App Check token (JWT). If the token is valid, the promise is
+        /// fulfilled with the token's decoded claims; otherwise, the promise is
+        /// rejected.
+        /// </summary>
+        /// <param name="appCheckToken"> TThe App Check token to verify.</param>
+        /// <param name="options"> Optional VerifyAppCheckTokenOptions object when verifying an App Check Token.</param>
+        /// <returns>A <see cref="Task{VerifyAppCheckTokenResponse}"/> representing the result of the asynchronous operation.</returns>
+        public async Task<AppCheckVerifyResponse> VerifyToken(string appCheckToken, VerifyAppCheckTokenOptions options = null)
+        {
+            if (string.IsNullOrEmpty(appCheckToken))
             {
-                throw new InvalidOperationException("invalid - argument" + error);
+                throw new ArgumentNullException("App check token " + appCheckToken + " must be a non - empty string.");
             }
 
-            byte[] hash;
-            using (var hashAlg = SHA256.Create())
+            FirebaseToken verifiedToken = await this.appCheckTokenVerifier.VerifyTokenAsync(appCheckToken).ConfigureAwait(false);
+            bool alreadyConsumed = await this.apiClient.VerifyReplayProtection(verifiedToken).ConfigureAwait(false);
+            AppCheckVerifyResponse result;
+
+            if (!alreadyConsumed)
             {
-                hash = hashAlg.ComputeHash(
-                    Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
+                result = new AppCheckVerifyResponse(verifiedToken.AppId, verifiedToken, alreadyConsumed);
             }
-
-            var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
-            var verified = cachedKeys.Any(key =>
-                key.Id == header.KeyId && key.RSA.VerifyHash(
-                    hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
-            if (verified)
+            else
             {
-                var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
-
-                // Remove standard claims, so that only custom claims would remain.
-                foreach (var claim in StandardClaims)
-                {
-                    allClaims.Remove(claim);
-                }
-
-                payload.Claims = allClaims.ToImmutableDictionary();
-                return Task.FromResult(new FirebaseToken(payload));
+                result = new AppCheckVerifyResponse(verifiedToken.AppId, verifiedToken);
             }
 
-            return Task.FromResult(new FirebaseToken(payload));
+            return result;
         }
 
         /// <summary>
-        /// Deleted all the apps created so far. Used for unit testing.
+        /// Deleted all the appChecks created so far. Used for unit testing.
         /// </summary>
-        public static void Delete()
+        internal static void DeleteAll()
         {
-            lock (cachedKeys)
-            {
-                var copy = new List<Auth.Jwt.PublicKey>(cachedKeys);
-                copy.Clear();
-
-                if (cachedKeys.Count > 0)
-                {
-                    throw new InvalidOperationException("Failed to delete all apps");
-                }
-            }
+            FirebaseApp.DeleteAll();
+            appChecks.Clear();
         }
     }
 }

From e2fabacd85959d605ee3641bd91ba95ec12d4fc1 Mon Sep 17 00:00:00 2001
From: pavlo <taeul930217@skiff.com>
Date: Thu, 25 Jan 2024 00:08:54 -0500
Subject: [PATCH 4/6] feature/appcheck/unit-test

---
 .../FirebaseAdmin.IntegrationTests.csproj     |   4 +-
 .../FirebaseAdmin.Snippets.csproj             |   2 +-
 .../AppCheck/AppCheckApiClientTest.cs         | 211 ++++++++++++++
 .../AppCheck/AppCheckTokenGeneratorTest.cs    | 112 ++++++++
 .../AppCheck/AppCheckTokenVerifierTest.cs     | 138 +++++++++
 .../Auth/Jwt/CustomTokenVerifier.cs           |   1 +
 .../Auth/Jwt/JwtTestUtils.cs                  |   3 +
 .../Auth/Jwt/ServiceAccountSignerTest.cs      |   1 +
 .../FirebaseAdmin.Tests.csproj                |  72 ++---
 .../FirebaseAppCheckTests.cs                  | 137 +++++++--
 .../{Check => AppCheck}/AppCheckApiClient.cs  |  32 +--
 .../{Check => AppCheck}/AppCheckToken.cs      |   0
 .../AppCheck/AppCheckTokenGenerator.cs        | 190 +++++++++++++
 .../Jwt => AppCheck}/AppCheckTokenOptions.cs  |   0
 .../AppCheck/AppCheckTokenVerify.cs           | 263 ++++++++++++++++++
 .../AppCheckVerifyResponse.cs                 |   2 +-
 .../{Check => AppCheck}/IAppCheckApiClient.cs |  10 +-
 .../AppCheck/IAppCheckTokenGenerator.cs       |  34 +++
 .../AppCheck/IAppCheckTokenVerify.cs          |  31 +++
 .../FirebaseAdmin/{Check => AppCheck}/Key.cs  |   0
 .../{Check => AppCheck}/KeysRoot.cs           |   0
 .../VerifyAppCheckTokenOptions.cs             |   0
 .../FirebaseAdmin/Auth/Jwt/ISigner.cs         |   6 +
 .../FirebaseAdmin/FirebaseAppCheck.cs         |  15 +-
 24 files changed, 1159 insertions(+), 105 deletions(-)
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/AppCheckApiClient.cs (89%)
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/AppCheckToken.cs (100%)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs
 rename FirebaseAdmin/FirebaseAdmin/{Auth/Jwt => AppCheck}/AppCheckTokenOptions.cs (100%)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/AppCheckVerifyResponse.cs (86%)
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/IAppCheckApiClient.cs (69%)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/Key.cs (100%)
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/KeysRoot.cs (100%)
 rename FirebaseAdmin/FirebaseAdmin/{Check => AppCheck}/VerifyAppCheckTokenOptions.cs (100%)

diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj
index 5c839131..278c7551 100644
--- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj
+++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <LangVersion>latest</LangVersion>
     <IsPackable>false</IsPackable>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -30,4 +30,4 @@
     </None>
   </ItemGroup>
 
-</Project>
+</Project>
\ No newline at end of file
diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj
index 75cedf49..b9161a93 100644
--- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj
+++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <IsPackable>false</IsPackable>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <CodeAnalysisRuleSet>../../stylecop_test.ruleset</CodeAnalysisRuleSet>
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs
new file mode 100644
index 00000000..c6f0b584
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs
@@ -0,0 +1,211 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FirebaseAdmin.Check;
+using Google.Apis.Auth.OAuth2;
+using Microsoft.VisualStudio.TestPlatform.ObjectModel;
+using Moq;
+using Xunit;
+
+namespace FirebaseAdmin.Tests.AppCheck
+{
+    public class AppCheckApiClientTest
+    {
+        private readonly string appId = "1:1234:android:1234";
+        private readonly string testTokenToExchange = "signed-custom-token";
+        private readonly string noProjectId = "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.";
+
+        [Fact]
+        public void CreateInvalidApp()
+        {
+            Assert.Throws<ArgumentException>(() => new AppCheckApiClient(null));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenNoProjectId()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException(this.noProjectId));
+            var result = await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
+            Assert.Equal(this.noProjectId, result.Message);
+        }
+
+        [Fact]
+        public async Task ExchangeTokenInvalidAppId()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException(this.noProjectId));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, string.Empty));
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, null));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenInvalidCustomTokenAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException(this.noProjectId));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(string.Empty, this.appId));
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(null, this.appId));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenFullErrorResponseAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException("not-found", "Requested entity not found"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenErrorCodeAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException("unknown-error", "Unknown server error: {}"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenFullNonJsonAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException("unknown-error", "Unexpected response with status: 404 and body: not json"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenAppErrorAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .Throws(new ArgumentException("network-error", "socket hang up"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(string.Empty, this.appId));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenOnSuccessAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .ReturnsAsync(new AppCheckToken("token", 3000));
+
+            var result = await appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId).ConfigureAwait(false);
+            Assert.NotNull(result);
+            Assert.Equal("token", result.Token);
+            Assert.Equal(3000, result.TtlMillis);
+        }
+
+        [Fact]
+        public async Task VerifyReplayNoProjectIdAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException(this.noProjectId));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+        }
+
+        [Fact]
+        public async Task VerifyReplayInvaildTokenAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException(this.noProjectId));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(string.Empty));
+        }
+
+        [Fact]
+        public async Task VerifyReplayFullErrorAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException("not-found", "Requested entity not found"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+        }
+
+        [Fact]
+        public async Task VerifyReplayErrorCodeAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException("unknown-error", "Unknown server error: {}"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+        }
+
+        [Fact]
+        public async Task VerifyReplayNonJsonAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException("unknown-error", "Unexpected response with status: 404 and body: not json"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+        }
+
+        [Fact]
+        public async Task VerifyReplayFirebaseAppErrorAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException("network-error", "socket hang up"));
+
+            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+        }
+
+        [Fact]
+        public async Task VerifyReplayAlreadyTrueAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .ReturnsAsync(true);
+
+            bool res = await appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange).ConfigureAwait(false);
+            Assert.True(res);
+        }
+
+        [Fact]
+        public async Task VerifyReplayAlreadyFlaseAsync()
+        {
+            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+
+            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .ReturnsAsync(true);
+
+            bool res = await appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange).ConfigureAwait(false);
+            Assert.True(res);
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs
new file mode 100644
index 00000000..96712720
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth.Jwt;
+using FirebaseAdmin.Check;
+using Google.Apis.Auth.OAuth2;
+using Moq;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace FirebaseAdmin.Tests.AppCheck
+{
+    public class AppCheckTokenGeneratorTest
+    {
+        public static readonly IEnumerable<object[]> InvalidStrings = new List<object[]>
+        {
+            new object[] { null },
+            new object[] { string.Empty },
+        };
+
+        private const int ThirtyMinInMs = 1800000;
+        private const int SevenDaysInMs = 604800000;
+        private static readonly GoogleCredential MockCredential =
+            GoogleCredential.FromAccessToken("test-token");
+
+        private readonly string appId = "test-app-id";
+
+        [Fact]
+        public void ProjectIdFromOptions()
+        {
+            var app = FirebaseApp.Create(new AppOptions()
+            {
+                Credential = MockCredential,
+                ProjectId = "explicit-project-id1",
+            });
+            var verifier = AppCheckTokenVerify.Create(app);
+            Assert.Equal("explicit-project-id1", verifier.ProjectId);
+        }
+
+        [Fact]
+        public void ProjectIdFromServiceAccount()
+        {
+            var app = FirebaseApp.Create(new AppOptions()
+            {
+                Credential = GoogleCredential.FromFile("./resources/service_account.json"),
+            });
+            var verifier = AppCheckTokenVerify.Create(app);
+            Assert.Equal("test-project", verifier.ProjectId);
+        }
+
+        [Fact]
+        public async Task InvalidAppId()
+        {
+            var options = new AppOptions()
+            {
+                Credential = GoogleCredential.FromAccessToken("token"),
+            };
+            var app = FirebaseApp.Create(options, "123");
+
+            AppCheckTokenGenerator tokenGenerator = AppCheckTokenGenerator.Create(app);
+            await Assert.ThrowsAsync<ArgumentException>(() => tokenGenerator.CreateCustomTokenAsync(string.Empty));
+            await Assert.ThrowsAsync<ArgumentException>(() => tokenGenerator.CreateCustomTokenAsync(null));
+        }
+
+        [Fact]
+        public async Task InvalidOptions()
+        {
+            var options = new AppOptions()
+            {
+                Credential = GoogleCredential.FromAccessToken("token"),
+            };
+            var app = FirebaseApp.Create(options, "1234");
+            var tokenGernerator = AppCheckTokenGenerator.Create(app);
+            int[] ttls = new int[] { -100, -1, 0, 10, 1799999, 604800001, 1209600000 };
+            foreach (var ttl in ttls)
+            {
+                var option = new AppCheckTokenOptions(ttl);
+
+                var result = await Assert.ThrowsAsync<ArgumentException>(() =>
+                    tokenGernerator.CreateCustomTokenAsync(this.appId, option));
+            }
+        }
+
+        [Fact]
+        public void ValidOptions()
+        {
+            var options = new AppOptions()
+            {
+                Credential = GoogleCredential.FromAccessToken("token"),
+            };
+            var app = FirebaseApp.Create(options, "12356");
+            var tokenGernerator = AppCheckTokenGenerator.Create(app);
+            int[] ttls = new int[] { ThirtyMinInMs, ThirtyMinInMs + 1, SevenDaysInMs / 2, SevenDaysInMs - 1, SevenDaysInMs };
+            foreach (var ttl in ttls)
+            {
+                var option = new AppCheckTokenOptions(ttl);
+
+                var result = tokenGernerator.CreateCustomTokenAsync(this.appId, option);
+                Assert.NotNull(result);
+            }
+        }
+
+        [Fact]
+        public void Dispose()
+        {
+            FirebaseApp.DeleteAll();
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs
new file mode 100644
index 00000000..80bc34c9
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth.Jwt;
+using FirebaseAdmin.Auth.Jwt.Tests;
+using FirebaseAdmin.Check;
+using Google.Apis.Auth.OAuth2;
+using Moq;
+using Xunit;
+
+namespace FirebaseAdmin.Tests.AppCheck
+{
+    public class AppCheckTokenVerifierTest
+    {
+        public static readonly IEnumerable<object[]> InvalidStrings = new List<object[]>
+        {
+            new object[] { null },
+            new object[] { string.Empty },
+        };
+
+        private static readonly GoogleCredential MockCredential =
+            GoogleCredential.FromAccessToken("test-token");
+
+        [Fact]
+        public void ProjectIdFromOptions()
+        {
+            var app = FirebaseApp.Create(new AppOptions()
+            {
+                Credential = MockCredential,
+                ProjectId = "explicit-project-id",
+            });
+            var verifier = AppCheckTokenVerify.Create(app);
+            Assert.Equal("explicit-project-id", verifier.ProjectId);
+        }
+
+        [Fact]
+        public void ProjectIdFromServiceAccount()
+        {
+            var app = FirebaseApp.Create(new AppOptions()
+            {
+                Credential = GoogleCredential.FromFile("./resources/service_account.json"),
+            });
+            var verifier = AppCheckTokenVerify.Create(app);
+            Assert.Equal("test-project", verifier.ProjectId);
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidStrings))]
+        public void InvalidProjectId(string projectId)
+        {
+            var args = FullyPopulatedArgs();
+            args.ProjectId = projectId;
+
+            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+        }
+
+        [Fact]
+        public void NullKeySource()
+        {
+            var args = FullyPopulatedArgs();
+            args.PublicKeySource = null;
+
+            Assert.Throws<ArgumentNullException>(() => new AppCheckTokenVerify(args));
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidStrings))]
+        public void InvalidShortName(string shortName)
+        {
+            var args = FullyPopulatedArgs();
+            args.ShortName = shortName;
+
+            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidStrings))]
+        public void InvalidIssuer(string issuer)
+        {
+            var args = FullyPopulatedArgs();
+            args.Issuer = issuer;
+
+            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidStrings))]
+        public void InvalidOperation(string operation)
+        {
+            var args = FullyPopulatedArgs();
+            args.Operation = operation;
+
+            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidStrings))]
+        public void InvalidUrl(string url)
+        {
+            var args = FullyPopulatedArgs();
+            args.Url = url;
+
+            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+        }
+
+        [Fact]
+        public void ProjectId()
+        {
+            var args = FullyPopulatedArgs();
+
+            var verifier = new AppCheckTokenVerify(args);
+
+            Assert.Equal("test-project", verifier.ProjectId);
+        }
+
+        [Fact]
+        public void Dispose()
+        {
+            FirebaseApp.DeleteAll();
+        }
+
+        private static FirebaseTokenVerifierArgs FullyPopulatedArgs()
+        {
+            return new FirebaseTokenVerifierArgs
+            {
+                ProjectId = "test-project",
+                ShortName = "short name",
+                Operation = "VerifyToken()",
+                Url = "https://firebase.google.com",
+                Issuer = "https://firebase.google.com/",
+                PublicKeySource = JwtTestUtils.DefaultKeySource,
+            };
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/CustomTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/CustomTokenVerifier.cs
index 314b28d7..d572a2eb 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/CustomTokenVerifier.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/CustomTokenVerifier.cs
@@ -18,6 +18,7 @@
 using System.Text;
 using Google.Apis.Auth;
 using Xunit;
+#pragma warning disable SYSLIB0027
 
 namespace FirebaseAdmin.Auth.Jwt.Tests
 {
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/JwtTestUtils.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/JwtTestUtils.cs
index db24f856..e57bbc46 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/JwtTestUtils.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/JwtTestUtils.cs
@@ -20,6 +20,7 @@
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading.Tasks;
+using FirebaseAdmin.Auth.Jwt;
 using FirebaseAdmin.Auth.Tests;
 using FirebaseAdmin.Tests;
 using FirebaseAdmin.Util;
@@ -27,6 +28,8 @@
 using Google.Apis.Util;
 using Xunit;
 
+#pragma warning disable SYSLIB0027
+
 namespace FirebaseAdmin.Auth.Jwt.Tests
 {
     public sealed class JwtTestUtils
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/ServiceAccountSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/ServiceAccountSignerTest.cs
index 630b688a..41ce2009 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/ServiceAccountSignerTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/ServiceAccountSignerTest.cs
@@ -20,6 +20,7 @@
 using System.Threading.Tasks;
 using Google.Apis.Auth.OAuth2;
 using Xunit;
+#pragma warning disable SYSLIB0027
 
 namespace FirebaseAdmin.Auth.Jwt.Tests
 {
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj
index 974f2237..c17a0b9f 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj
@@ -1,41 +1,41 @@
 <Project Sdk="Microsoft.NET.Sdk">
+	<PropertyGroup>
+		<TargetFramework>net7.0</TargetFramework>
+		<LangVersion>latest</LangVersion>
+		<IsPackable>false</IsPackable>
+		<AssemblyOriginatorKeyFile>../../FirebaseAdmin.snk</AssemblyOriginatorKeyFile>
+		<SignAssembly>true</SignAssembly>
+		<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+		<CodeAnalysisRuleSet>../../stylecop_test.ruleset</CodeAnalysisRuleSet>
+	</PropertyGroup>
 
-  <PropertyGroup>
-    <TargetFramework>netcoreapp2.1</TargetFramework>
-    <LangVersion>latest</LangVersion>
-    <IsPackable>false</IsPackable>
-    <AssemblyOriginatorKeyFile>../../FirebaseAdmin.snk</AssemblyOriginatorKeyFile>
-    <SignAssembly>true</SignAssembly>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-    <CodeAnalysisRuleSet>../../stylecop_test.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
+	<ItemGroup>
+		<PackageReference Include="coverlet.msbuild" Version="2.9.0">
+			<PrivateAssets>all</PrivateAssets>
+			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+		</PackageReference>
+		<PackageReference Include="Google.Apis.Auth" Version="1.49.0" />
+		<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+		<PackageReference Include="Moq" Version="4.20.70" />
+		<PackageReference Include="System.Linq.Async" Version="4.1.1" />
+		<PackageReference Include="xunit" Version="2.4.1" />
+		<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+			<PrivateAssets>all</PrivateAssets>
+			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+		</PackageReference>
+		<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
+		<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
+			<PrivateAssets>all</PrivateAssets>
+		</PackageReference>
+	</ItemGroup>
 
-  <ItemGroup>
-    <PackageReference Include="coverlet.msbuild" Version="2.9.0">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
-    <PackageReference Include="Google.Apis.Auth" Version="1.49.0" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
-    <PackageReference Include="System.Linq.Async" Version="4.1.1" />
-    <PackageReference Include="xunit" Version="2.4.1" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
-      <PrivateAssets>all</PrivateAssets>
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-    </PackageReference>
-    <DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
-      <PrivateAssets>all</PrivateAssets>
-    </PackageReference>
-  </ItemGroup>
-
-  <ItemGroup>
-    <ProjectReference Include="..\FirebaseAdmin\FirebaseAdmin.csproj" />
-  </ItemGroup>
-  <ItemGroup>
-    <None Update=".\resources\*">
-      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-    </None>
-  </ItemGroup>
+	<ItemGroup>
+		<ProjectReference Include="..\FirebaseAdmin\FirebaseAdmin.csproj" />
+	</ItemGroup>
+	<ItemGroup>
+		<None Update=".\resources\*">
+			<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+		</None>
+	</ItemGroup>
 
 </Project>
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
index 53814bd9..7ffa46a0 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
@@ -1,18 +1,28 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Net;
 using System.Net.Http;
 using System.Reflection.Metadata.Ecma335;
+using System.Runtime.CompilerServices;
+using System.Threading;
 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;
 using Newtonsoft.Json.Linq;
 using Xunit;
 
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, " +
+    "PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93" +
+    "bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113b" +
+    "e11e6a7d3113e92484cf7045cc7")]
+
 namespace FirebaseAdmin.Tests
 {
     public class FirebaseAppCheckTests : IDisposable
@@ -31,19 +41,16 @@ public FirebaseAppCheckTests()
         }
 
         [Fact]
-        public void CreateAppCheck()
+        public void CreateInvalidApp()
         {
-            FirebaseAppCheck withoutAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
-            Assert.NotNull(withoutAppIdCreate);
+            Assert.Throws<ArgumentNullException>(() => FirebaseAppCheck.Create(null));
         }
 
         [Fact]
-        public async Task InvalidAppIdCreateToken()
+        public void CreateAppCheck()
         {
-            FirebaseAppCheck invalidAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
-
-            await Assert.ThrowsAsync<ArgumentException>(() => invalidAppIdCreate.CreateToken(appId: null));
-            await Assert.ThrowsAsync<ArgumentException>(() => invalidAppIdCreate.CreateToken(appId: string.Empty));
+            FirebaseAppCheck withoutAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
+            Assert.NotNull(withoutAppIdCreate);
         }
 
         [Fact]
@@ -63,37 +70,115 @@ public void WithoutProjectIDCreate()
         }
 
         [Fact]
-        public async Task CreateTokenFromAppId()
+        public void FailedSignCreateToken()
         {
+            string expected = "sign error";
+            var createTokenMock = new Mock<ISigner>();
+
+            // Setup the mock to throw an exception when SignDataAsync is called
+            createTokenMock.Setup(service => service.SignDataAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
+                           .Throws(new ArgumentException(expected));
+
+            var options = new AppOptions()
+            {
+                Credential = GoogleCredential.FromAccessToken("token"),
+            };
+            var app = FirebaseApp.Create(options, "4321");
+
+            Assert.Throws<ArgumentException>(() => FirebaseAppCheck.Create(app));
+        }
+
+        [Fact]
+        public async Task CreateTokenApiError()
+        {
+            var createTokenMock = new Mock<IAppCheckApiClient>();
+
+            createTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>())).Throws(new ArgumentException("INTERAL_ERROR"));
+
             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);
+
+            await Assert.ThrowsAsync<HttpRequestException>(() => createTokenFromAppId.CreateToken(this.appId));
         }
 
         [Fact]
-        public async Task CreateTokenFromAppIdAndTtlMillis()
+        public async Task CreateTokenApiErrorOptions()
         {
+            var createTokenMock = new Mock<IAppCheckApiClient>();
+
+            createTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>())).Throws(new ArgumentException("INTERAL_ERROR"));
+
             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);
+            await Assert.ThrowsAsync<HttpRequestException>(() => createTokenFromAppIdAndTtlMillis.CreateToken(this.appId));
         }
 
         [Fact]
-        public async Task VerifyToken()
+        public async Task CreateTokenAppCheckTokenSuccess()
         {
+            string createdCustomToken = "custom-token";
+
+            AppCheckTokenGenerator tokenFactory = AppCheckTokenGenerator.Create(this.mockCredentialApp);
+
+            var createCustomTokenMock = new Mock<IAppCheckTokenGenerator>();
+
+            createCustomTokenMock.Setup(service => service.CreateCustomTokenAsync(It.IsAny<string>(), It.IsAny<AppCheckTokenOptions>(), It.IsAny<CancellationToken>()))
+                .ReturnsAsync(createdCustomToken);
+
+            var customRes = await createCustomTokenMock.Object.CreateCustomTokenAsync(this.appId).ConfigureAwait(false);
+            Assert.Equal(createdCustomToken, customRes);
+
+            AppCheckToken expected = new ("token", 3000);
+            var createExchangeTokenMock = new Mock<IAppCheckApiClient>();
+            createExchangeTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
+                .ReturnsAsync(expected);
+
+            AppCheckTokenOptions options = new (3000);
+
+            AppCheckToken res = await createExchangeTokenMock.Object.ExchangeTokenAsync("custom-token", this.appId).ConfigureAwait(false);
+            Assert.Equal("token", res.Token);
+            Assert.Equal(3000, res.TtlMillis);
+        }
+
+        [Fact]
+        public async Task VerifyTokenApiError()
+        {
+            var createTokenMock = new Mock<IAppCheckApiClient>();
+            createTokenMock.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
+                .Throws(new ArgumentException("INTERAL_ERROR"));
+
             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);
+            await Assert.ThrowsAsync<ArgumentException>(() => createTokenMock.Object.VerifyReplayProtection("token"));
+        }
+
+        [Fact]
+        public async Task VerifyTokenSuccess()
+        {
+            // Create an instance of FirebaseToken.Args and set its properties.
+            var args = new FirebaseToken.Args
+            {
+                AppId = "1234",
+                Issuer = "issuer",
+                Subject = "subject",
+                Audience = "audience",
+                ExpirationTimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 3600, // 1 hour from now
+                IssuedAtTimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
+            };
+            FirebaseToken mockFirebaseToken = new FirebaseToken(args);
+
+            var verifyTokenMock = new Mock<IAppCheckTokenVerify>();
+            verifyTokenMock.Setup(service => service.VerifyTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
+                .ReturnsAsync(mockFirebaseToken);
+
+            var verifyRes = await verifyTokenMock.Object.VerifyTokenAsync(this.appId).ConfigureAwait(false);
+
+            Assert.Equal(verifyRes.AppId, mockFirebaseToken.AppId);
+            Assert.Equal(verifyRes.Issuer, mockFirebaseToken.Issuer);
+            Assert.Equal(verifyRes.Subject, mockFirebaseToken.Subject);
+            Assert.Equal(verifyRes.Audience, mockFirebaseToken.Audience);
+            Assert.Equal(verifyRes.ExpirationTimeSeconds, mockFirebaseToken.ExpirationTimeSeconds);
+            Assert.Equal(verifyRes.IssuedAtTimeSeconds, mockFirebaseToken.IssuedAtTimeSeconds);
         }
 
         [Fact]
@@ -101,8 +186,8 @@ public async Task VerifyTokenInvaild()
         {
             FirebaseAppCheck verifyTokenInvaild = new FirebaseAppCheck(this.mockCredentialApp);
 
-            await Assert.ThrowsAsync<ArgumentException>(() => verifyTokenInvaild.VerifyToken(null));
-            await Assert.ThrowsAsync<ArgumentException>(() => verifyTokenInvaild.VerifyToken(string.Empty));
+            await Assert.ThrowsAsync<ArgumentNullException>(() => verifyTokenInvaild.VerifyToken(null));
+            await Assert.ThrowsAsync<ArgumentNullException>(() => verifyTokenInvaild.VerifyToken(string.Empty));
         }
 
         public void Dispose()
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
similarity index 89%
rename from FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
index fa909b55..13e039b4 100644
--- a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckApiClient.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
@@ -51,31 +51,14 @@ public async Task<AppCheckToken> ExchangeTokenAsync(string customToken, string a
         {
             if (string.IsNullOrEmpty(customToken))
             {
-                throw new ArgumentException("First argument passed to customToken must be a valid Firebase app instance.");
+                throw new ArgumentNullException("First argument passed to customToken must be a valid Firebase app instance.");
             }
 
             if (string.IsNullOrEmpty(appId))
             {
-                throw new ArgumentException("Second argument passed to appId must be a valid Firebase app instance.");
+                throw new ArgumentNullException("Second argument passed to appId must be a valid Firebase app instance.");
             }
 
-            HttpResponseMessage response = await this.GetExchangeToken(customToken, appId).ConfigureAwait(false);
-
-            JObject responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
-            string tokenValue = responseData["data"]["token"].ToString();
-            int ttlValue = this.StringToMilliseconds(responseData["data"]["ttl"].ToString());
-            AppCheckToken appCheckToken = new (tokenValue, ttlValue);
-            return appCheckToken;
-        }
-
-        /// <summary>
-        /// Exchange a signed custom token to App Check token.
-        /// </summary>
-        /// <param name="customToken"> The custom token to be exchanged. </param>
-        /// <param name="appId"> The Id of Firebase App. </param>
-        /// <returns> HttpResponseMessage .</returns>
-        public async Task<HttpResponseMessage> GetExchangeToken(string customToken, string appId)
-        {
             var url = this.GetUrl(appId);
             var content = new StringContent(JsonConvert.SerializeObject(new { customToken }), Encoding.UTF8, "application/json");
             var request = new HttpRequestMessage()
@@ -85,7 +68,6 @@ public async Task<HttpResponseMessage> GetExchangeToken(string customToken, stri
                 Content = content,
             };
             request.Headers.Add("X-Firebase-Client", "fire-admin-node/" + $"{FirebaseApp.GetSdkVersion()}");
-            Console.WriteLine(request.Content);
             var httpClient = new HttpClient();
             var response = await httpClient.SendAsync(request).ConfigureAwait(false);
             if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
@@ -95,10 +77,14 @@ public async Task<HttpResponseMessage> GetExchangeToken(string customToken, stri
             }
             else if (!response.IsSuccessStatusCode)
             {
-                throw new ArgumentException("unknown-error", $"Unexpected response with status:{response.StatusCode}");
+                throw new HttpRequestException("network error");
             }
 
-            return response;
+            JObject responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
+            string tokenValue = responseData["data"]["token"].ToString();
+            int ttlValue = this.StringToMilliseconds(responseData["data"]["ttl"].ToString());
+            AppCheckToken appCheckToken = new (tokenValue, ttlValue);
+            return appCheckToken;
         }
 
         /// <summary>
@@ -128,7 +114,7 @@ public async Task<bool> VerifyReplayProtection(string token)
             var responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
             bool alreadyConsumed = (bool)responseData["data"]["alreadyConsumed"];
             return alreadyConsumed;
-          }
+        }
 
         /// <summary>
         /// Get Verify Token Url .
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckToken.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckToken.cs
similarity index 100%
rename from FirebaseAdmin/FirebaseAdmin/Check/AppCheckToken.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckToken.cs
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs
new file mode 100644
index 00000000..6c0e7bf1
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth;
+using FirebaseAdmin.Auth.Jwt;
+using Google.Apis.Auth;
+using Google.Apis.Util;
+using Newtonsoft.Json;
+
+[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" +
+"002400000480000094000000060200000024000052534131000400000100010081328559eaab41" +
+"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02" +
+"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597" +
+"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f" +
+"badddb9c")]
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// A helper class that creates Firebase custom tokens.
+    /// </summary>
+    internal class AppCheckTokenGenerator
+    {
+        public const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
+           + "google.identity.identitytoolkit.v1.IdentityToolkit";
+
+        public const string FirebaseAppCheckAudience = "https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService";
+
+        public const int OneMinuteInSeconds = 60;
+        public const int OneMinuteInMills = OneMinuteInSeconds * 1000;
+        public const int OneDayInMills = 24 * 60 * 60 * 1000;
+
+        public static readonly DateTime UnixEpoch = new DateTime(
+            1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+        public static readonly ImmutableList<string> ReservedClaims = ImmutableList.Create(
+            "acr",
+            "amr",
+            "at_hash",
+            "aud",
+            "auth_time",
+            "azp",
+            "cnf",
+            "c_hash",
+            "exp",
+            "firebase",
+            "iat",
+            "iss",
+            "jti",
+            "nbf",
+            "nonce",
+            "sub");
+
+        internal AppCheckTokenGenerator(Args args)
+        {
+            args.ThrowIfNull(nameof(args));
+
+            this.Clock = args.Clock ?? SystemClock.Default;
+            this.IsEmulatorMode = args.IsEmulatorMode;
+            this.Signer = this.IsEmulatorMode ?
+                EmulatorSigner.Instance : args.Signer.ThrowIfNull(nameof(args.Signer));
+        }
+
+        internal ISigner Signer { get; }
+
+        internal IClock Clock { get; }
+
+        internal string TenantId { get; }
+
+        internal bool IsEmulatorMode { get; }
+
+        public void Dispose()
+        {
+            this.Signer.Dispose();
+        }
+
+        internal static AppCheckTokenGenerator Create(FirebaseApp app)
+        {
+            ISigner signer = null;
+            var serviceAccount = app.Options.Credential.ToServiceAccountCredential();
+            if (serviceAccount != null)
+            {
+                // If the app was initialized with a service account, use it to sign
+                // tokens locally.
+                signer = new ServiceAccountSigner(serviceAccount);
+            }
+            else if (string.IsNullOrEmpty(app.Options.ServiceAccountId))
+            {
+                // If no service account ID is specified, attempt to discover one and invoke the
+                // IAM service with it.
+                signer = IAMSigner.Create(app);
+            }
+            else
+            {
+                // If a service account ID is specified, invoke the IAM service with it.
+                signer = FixedAccountIAMSigner.Create(app);
+            }
+
+            var args = new Args
+            {
+                Signer = signer,
+                IsEmulatorMode = Utils.IsEmulatorModeFromEnvironment,
+            };
+            return new AppCheckTokenGenerator(args);
+        }
+
+        internal async Task<string> CreateCustomTokenAsync(
+            string appId,
+            AppCheckTokenOptions options = null,
+            CancellationToken cancellationToken = default)
+        {
+            if (string.IsNullOrEmpty(appId))
+            {
+                throw new ArgumentException("appId must not be null or empty");
+            }
+            else if (appId.Length > 128)
+            {
+                throw new ArgumentException("appId must not be longer than 128 characters");
+            }
+
+            string customOptions = " ";
+            if (options != null)
+            {
+                customOptions = this.ValidateTokenOptions(options).ToString();
+            }
+
+            var header = new JsonWebSignature.Header()
+            {
+                Algorithm = this.Signer.Algorithm,
+                Type = "JWT",
+            };
+
+            var issued = (int)(this.Clock.UtcNow - UnixEpoch).TotalSeconds;
+            var keyId = await this.Signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
+            var payload = new CustomTokenPayload()
+            {
+                AppId = appId,
+                Issuer = keyId,
+                Subject = keyId,
+                Audience = FirebaseAppCheckAudience,
+                IssuedAtTimeSeconds = issued,
+                ExpirationTimeSeconds = issued + (OneMinuteInSeconds * 5),
+                Ttl = customOptions,
+            };
+
+            return await JwtUtils.CreateSignedJwtAsync(
+                header, payload, this.Signer).ConfigureAwait(false);
+        }
+
+        private int ValidateTokenOptions(AppCheckTokenOptions options)
+        {
+            if (options == null)
+            {
+                throw new ArgumentException("invalid-argument", "AppCheckTokenOptions must be a non-null object.");
+            }
+
+            if (options.TtlMillis < (OneMinuteInMills * 30) || options.TtlMillis > (OneDayInMills * 7))
+            {
+                throw new ArgumentException("invalid-argument", "ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).");
+            }
+
+            return options.TtlMillis;
+        }
+
+        internal class CustomTokenPayload : JsonWebToken.Payload
+        {
+            [JsonPropertyAttribute("app_id")]
+            public string AppId { get; set; }
+
+            [JsonPropertyAttribute("ttl")]
+            public string Ttl { get; set; }
+        }
+
+        internal sealed class Args
+        {
+            internal ISigner Signer { get; set; }
+
+            internal IClock Clock { get; set; }
+
+            internal string TenantId { get; set; }
+
+            internal bool IsEmulatorMode { get; set; }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/AppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenOptions.cs
similarity index 100%
rename from FirebaseAdmin/FirebaseAdmin/Auth/Jwt/AppCheckTokenOptions.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenOptions.cs
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs
new file mode 100644
index 00000000..b11334b3
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth;
+using FirebaseAdmin.Auth.Jwt;
+using Google.Apis.Auth;
+using Google.Apis.Util;
+
+namespace FirebaseAdmin.Check
+{
+    internal class AppCheckTokenVerify
+    {
+        /*private const string IdTokenCertUrl = "https://www.googleapis.com/robot/v1/metadata/x509/"
+            + "securetoken@system.gserviceaccount.com";
+
+        private const string SessionCookieCertUrl = "https://www.googleapis.com/identitytoolkit/v3/"
+            + "relyingparty/publicKeys";
+*/
+        private const string AppCheckIuuser = "https://firebaseappcheck.googleapis.com/";
+        private const string JWKSURL = "https://firebaseappcheck.googleapis.com/v1/jwks";
+        private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
+            + "google.identity.identitytoolkit.v1.IdentityToolkit";
+
+        private const long ClockSkewSeconds = 5 * 60;
+
+        // See http://oid-info.com/get/2.16.840.1.101.3.4.2.1
+        private const string Sha256Oid = "2.16.840.1.101.3.4.2.1";
+
+        private static readonly IReadOnlyList<string> StandardClaims =
+            ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
+
+        private readonly string shortName;
+        private readonly string articledShortName;
+        private readonly string operation;
+        private readonly string url;
+        private readonly string issuer;
+        private readonly IClock clock;
+        private readonly IPublicKeySource keySource;
+        private readonly AuthErrorCode invalidTokenCode;
+        private readonly AuthErrorCode expiredIdTokenCode;
+
+        internal AppCheckTokenVerify(FirebaseTokenVerifierArgs args)
+        {
+            this.ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId));
+            this.shortName = args.ShortName.ThrowIfNullOrEmpty(nameof(args.ShortName));
+            this.operation = args.Operation.ThrowIfNullOrEmpty(nameof(args.Operation));
+            this.url = args.Url.ThrowIfNullOrEmpty(nameof(args.Url));
+            this.issuer = args.Issuer.ThrowIfNullOrEmpty(nameof(args.Issuer));
+            this.clock = args.Clock ?? SystemClock.Default;
+            this.keySource = args.PublicKeySource.ThrowIfNull(nameof(args.PublicKeySource));
+            this.invalidTokenCode = args.InvalidTokenCode;
+            this.expiredIdTokenCode = args.ExpiredTokenCode;
+            this.IsEmulatorMode = args.IsEmulatorMode;
+            if ("aeiou".Contains(this.shortName.ToLower().Substring(0, 1)))
+            {
+                this.articledShortName = $"an {this.shortName}";
+            }
+            else
+            {
+                this.articledShortName = $"a {this.shortName}";
+            }
+        }
+
+        internal string ProjectId { get; }
+
+        internal bool IsEmulatorMode { get; }
+
+        internal static AppCheckTokenVerify Create(FirebaseApp app)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            var projectId = app.GetProjectId();
+            if (string.IsNullOrEmpty(projectId))
+            {
+                throw new ArgumentException(
+                    "Must initialize FirebaseApp with a project ID to verify session cookies.");
+            }
+
+            var keySource = new HttpPublicKeySource(
+                JWKSURL, SystemClock.Default, app.Options.HttpClientFactory);
+            return Create(projectId, keySource);
+        }
+
+        internal static AppCheckTokenVerify Create(
+            string projectId,
+            IPublicKeySource keySource,
+            IClock clock = null)
+        {
+            var args = new FirebaseTokenVerifierArgs()
+            {
+                ProjectId = projectId,
+                ShortName = "session cookie",
+                Operation = "VerifySessionCookieAsync()",
+                Url = "https://firebase.google.com/docs/auth/admin/manage-cookies",
+                Issuer = "https://session.firebase.google.com/",
+                Clock = clock,
+                PublicKeySource = keySource,
+                InvalidTokenCode = AuthErrorCode.InvalidSessionCookie,
+                ExpiredTokenCode = AuthErrorCode.ExpiredSessionCookie,
+            };
+            return new AppCheckTokenVerify(args);
+        }
+
+        internal async Task<FirebaseToken> VerifyTokenAsync(
+            string token, CancellationToken cancellationToken = default(CancellationToken))
+        {
+            if (string.IsNullOrEmpty(token))
+            {
+                throw new ArgumentException($"{this.shortName} must not be null or empty.");
+            }
+
+            string[] segments = token.Split('.');
+            if (segments.Length != 3)
+            {
+                throw this.CreateException($"Incorrect number of segments in {this.shortName}.");
+            }
+
+            var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
+            var payload = JwtUtils.Decode<FirebaseToken.Args>(segments[1]);
+            var projectIdMessage = $"Make sure the {this.shortName} comes from the same Firebase "
+                + "project as the credential used to initialize this SDK.";
+            var verifyTokenMessage = $"See {this.url} for details on how to retrieve a value "
+                + $"{this.shortName}.";
+            var issuer = this.issuer + this.ProjectId;
+            string error = null;
+            var errorCode = this.invalidTokenCode;
+            var currentTimeInSeconds = this.clock.UnixTimestamp();
+
+            if (!this.IsEmulatorMode && string.IsNullOrEmpty(header.KeyId))
+            {
+                if (payload.Audience == FirebaseAudience)
+                {
+                    error = $"{this.operation} expects {this.articledShortName}, but was given a custom "
+                        + "token.";
+                }
+                else if (header.Algorithm == "HS256")
+                {
+                    error = $"{this.operation} expects {this.articledShortName}, but was given a legacy "
+                        + "custom token.";
+                }
+                else
+                {
+                    error = $"Firebase {this.shortName} has no 'kid' claim.";
+                }
+            }
+            else if (!this.IsEmulatorMode && header.Algorithm != "RS256")
+            {
+                error = $"Firebase {this.shortName} has incorrect algorithm. Expected RS256 but got "
+                    + $"{header.Algorithm}. {verifyTokenMessage}";
+            }
+            else if (this.ProjectId != payload.Audience)
+            {
+                error = $"Firebase {this.shortName} has incorrect audience (aud) claim. Expected "
+                    + $"{this.ProjectId} but got {payload.Audience}. {projectIdMessage} "
+                    + $"{verifyTokenMessage}";
+            }
+            else if (payload.Issuer != issuer)
+            {
+                error = $"Firebase {this.shortName} has incorrect issuer (iss) claim. Expected "
+                    + $"{issuer} but got {payload.Issuer}.  {projectIdMessage} {verifyTokenMessage}";
+            }
+            else if (payload.IssuedAtTimeSeconds - ClockSkewSeconds > currentTimeInSeconds)
+            {
+                error = $"Firebase {this.shortName} issued at future timestamp "
+                    + $"{payload.IssuedAtTimeSeconds}. Expected to be less than "
+                    + $"{currentTimeInSeconds}.";
+            }
+            else if (payload.ExpirationTimeSeconds + ClockSkewSeconds < currentTimeInSeconds)
+            {
+                error = $"Firebase {this.shortName} expired at {payload.ExpirationTimeSeconds}. "
+                    + $"Expected to be greater than {currentTimeInSeconds}.";
+                errorCode = this.expiredIdTokenCode;
+            }
+            else if (string.IsNullOrEmpty(payload.Subject))
+            {
+                error = $"Firebase {this.shortName} has no or empty subject (sub) claim.";
+            }
+            else if (payload.Subject.Length > 128)
+            {
+                error = $"Firebase {this.shortName} has a subject claim longer than 128 characters.";
+            }
+
+            if (error != null)
+            {
+                throw this.CreateException(error, errorCode);
+            }
+
+            await this.VerifySignatureAsync(segments, header.KeyId, cancellationToken)
+                .ConfigureAwait(false);
+            var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
+
+            // Remove standard claims, so that only custom claims would remain.
+            foreach (var claim in StandardClaims)
+            {
+                allClaims.Remove(claim);
+            }
+
+            payload.Claims = allClaims.ToImmutableDictionary();
+            return new FirebaseToken(payload);
+        }
+
+        /// <summary>
+        /// Verifies the integrity of a JWT by validating its signature. The JWT must be specified
+        /// as an array of three segments (header, body and signature).
+        /// </summary>
+        [SuppressMessage(
+            "StyleCop.Analyzers",
+            "SA1009:ClosingParenthesisMustBeSpacedCorrectly",
+            Justification = "Use of directives.")]
+        [SuppressMessage(
+            "StyleCop.Analyzers",
+            "SA1111:ClosingParenthesisMustBeOnLineOfLastParameter",
+            Justification = "Use of directives.")]
+        private async Task VerifySignatureAsync(
+            string[] segments, string keyId, CancellationToken cancellationToken)
+        {
+            if (this.IsEmulatorMode)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                return;
+            }
+
+            byte[] hash;
+            using (var hashAlg = SHA256.Create())
+            {
+                hash = hashAlg.ComputeHash(
+                    Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
+            }
+
+            var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
+            var keys = await this.keySource.GetPublicKeysAsync(cancellationToken)
+                .ConfigureAwait(false);
+            var verified = keys.Any(key =>
+                key.Id == keyId && key.RSA.VerifyHash(
+                    hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
+            );
+            if (!verified)
+            {
+                throw this.CreateException($"Failed to verify {this.shortName} signature.");
+            }
+        }
+
+        private FirebaseAuthException CreateException(
+            string message, AuthErrorCode? errorCode = null)
+        {
+            if (errorCode == null)
+            {
+                errorCode = this.invalidTokenCode;
+            }
+
+            return new FirebaseAuthException(ErrorCode.InvalidArgument, message, errorCode);
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs
similarity index 86%
rename from FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs
index 90ae92f4..8cc05641 100644
--- a/FirebaseAdmin/FirebaseAdmin/Check/AppCheckVerifyResponse.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs
@@ -8,7 +8,7 @@ namespace FirebaseAdmin.Check
     /// <summary>
     /// AppCheckVerifyResponse.
     /// </summary>
-    public class AppCheckVerifyResponse(string appId, FirebaseToken verifiedToken, bool alreadyConsumed = false)
+    public class AppCheckVerifyResponse(string appId, string verifiedToken, bool alreadyConsumed = false)
     {
         /// <summary>
         /// Gets or sets a value indicating whether gets the Firebase App Check token.
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs
similarity index 69%
rename from FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs
index 2e281079..0ea2eb8a 100644
--- a/FirebaseAdmin/FirebaseAdmin/Check/IAppCheckApiClient.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs
@@ -9,7 +9,7 @@ namespace FirebaseAdmin.Check
     /// <summary>
     /// Interface of Firebase App Check backend API .
     /// </summary>
-    internal interface IAppCheckApiClient
+    public interface IAppCheckApiClient
     {
         /// <summary>
         /// Exchange a signed custom token to App Check token.
@@ -25,13 +25,5 @@ internal interface IAppCheckApiClient
         /// <param name="token"> The custom token to be exchanged. </param>
         /// <returns>A alreadyConsumed is true.</returns>
         public Task<bool> VerifyReplayProtection(string token);
-
-        /// <summary>
-        /// Exchange a signed custom token to App Check token.
-        /// </summary>
-        /// <param name="customToken"> The custom token to be exchanged. </param>
-        /// <param name="appId"> The Id of Firebase App. </param>
-        /// <returns> HttpResponseMessage .</returns>
-        public Task<HttpResponseMessage> GetExchangeToken(string customToken, string appId);
     }
 }
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs
new file mode 100644
index 00000000..a24554b9
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth.Jwt;
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// App Check Token generator.
+    /// </summary>
+    internal interface IAppCheckTokenGenerator
+    {
+        /// <summary>
+        /// Verifies the integrity of a JWT by validating its signature.
+        /// </summary>
+        /// <param name="app">Appcheck Generate Token.</param>
+        /// <returns>AppCheckTokenVerify.</returns>
+        AppCheckTokenGenerator Create(FirebaseApp app);
+
+        /// <summary>
+        /// Verifies the integrity of a JWT by validating its signature.
+        /// </summary>
+        /// <param name="appId">The Id of FirebaseApp.</param>
+        /// <param name="options">AppCheck Token Option.</param>
+        /// <param name="cancellationToken">Cancelation Token.</param>
+        /// <returns>A <see cref="Task{FirebaseToken}"/> representing the result of the asynchronous operation.</returns>
+        Task<string> CreateCustomTokenAsync(
+            string appId,
+            AppCheckTokenOptions options = null,
+            CancellationToken cancellationToken = default);
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs
new file mode 100644
index 00000000..cc01d99b
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth;
+
+namespace FirebaseAdmin.Check
+{
+    /// <summary>
+    /// App Check Verify.
+    /// </summary>
+    internal interface IAppCheckTokenVerify
+    {
+        /// <summary>
+        /// Verifies the integrity of a JWT by validating its signature.
+        /// </summary>
+        /// <param name="app">Appcheck Generate Token.</param>
+        /// <returns>AppCheckTokenVerify.</returns>
+        AppCheckTokenVerify Create(FirebaseApp app);
+
+        /// <summary>
+        /// Verifies the integrity of a JWT by validating its signature.
+        /// </summary>
+        /// <param name="token">Appcheck Generate Token.</param>
+        /// <param name="cancellationToken">cancellaton Token.</param>
+        /// <returns>A <see cref="Task{FirebaseToken}"/> representing the result of the asynchronous operation.</returns>
+        Task<FirebaseToken> VerifyTokenAsync(
+            string token, CancellationToken cancellationToken = default(CancellationToken));
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/Key.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs
similarity index 100%
rename from FirebaseAdmin/FirebaseAdmin/Check/Key.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/KeysRoot.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/KeysRoot.cs
similarity index 100%
rename from FirebaseAdmin/FirebaseAdmin/Check/KeysRoot.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/KeysRoot.cs
diff --git a/FirebaseAdmin/FirebaseAdmin/Check/VerifyAppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/VerifyAppCheckTokenOptions.cs
similarity index 100%
rename from FirebaseAdmin/FirebaseAdmin/Check/VerifyAppCheckTokenOptions.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/VerifyAppCheckTokenOptions.cs
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/ISigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/ISigner.cs
index b601af26..9387cc3a 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/ISigner.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/ISigner.cs
@@ -13,9 +13,15 @@
 // limitations under the License.
 
 using System;
+using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading.Tasks;
 
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, " +
+    "PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93" +
+    "bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113b" +
+    "e11e6a7d3113e92484cf7045cc7")]
+
 namespace FirebaseAdmin.Auth.Jwt
 {
     /// <summary>
diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
index 0710bfa8..b1a6d836 100644
--- a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
+++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Xml.Linq;
 using FirebaseAdmin.Auth;
@@ -21,8 +22,8 @@ public sealed class FirebaseAppCheck
         private static Dictionary<string, FirebaseAppCheck> appChecks = new Dictionary<string, FirebaseAppCheck>();
 
         private readonly AppCheckApiClient apiClient;
-        private readonly FirebaseTokenVerifier appCheckTokenVerifier;
-        private readonly FirebaseTokenFactory tokenFactory;
+        private readonly AppCheckTokenVerify appCheckTokenVerifier;
+        private readonly AppCheckTokenGenerator tokenFactory;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="FirebaseAppCheck"/> class.
@@ -31,8 +32,8 @@ public sealed class FirebaseAppCheck
         public FirebaseAppCheck(FirebaseApp value)
         {
             this.apiClient = new AppCheckApiClient(value);
-            this.tokenFactory = FirebaseTokenFactory.Create(value);
-            this.appCheckTokenVerifier = FirebaseTokenVerifier.CreateAppCheckVerifier(value);
+            this.tokenFactory = AppCheckTokenGenerator.Create(value);
+            this.appCheckTokenVerifier = AppCheckTokenVerify.Create(value);
         }
 
         /// <summary>
@@ -42,13 +43,13 @@ public FirebaseAppCheck(FirebaseApp value)
         /// <returns>A <see cref="Task{FirebaseAppCheck}"/> Representing the result of the asynchronous operation.</returns>
         public static FirebaseAppCheck Create(FirebaseApp app)
         {
-            string appId = app.Name;
-
             if (app == null)
             {
                 throw new ArgumentNullException("FirebaseApp must not be null or empty");
             }
 
+            string appId = app.Name;
+
             lock (appChecks)
             {
                 if (appChecks.ContainsKey(appId))
@@ -77,7 +78,7 @@ public static FirebaseAppCheck Create(FirebaseApp app)
         /// <returns>A <see cref="Task{AppCheckToken}"/> Representing the result of the asynchronous operation.</returns>
         public async Task<AppCheckToken> CreateToken(string appId, AppCheckTokenOptions options = null)
         {
-            string customToken = await this.tokenFactory.CreateCustomTokenAppIdAsync(appId, options)
+            string customToken = await this.tokenFactory.CreateCustomTokenAsync(appId, options, default(CancellationToken))
                 .ConfigureAwait(false);
 
             return await this.apiClient.ExchangeTokenAsync(customToken, appId).ConfigureAwait(false);

From 28f60b9218b624e1217d73fa9b0517d73524b42c Mon Sep 17 00:00:00 2001
From: pavlo <taeul930217@skiff.com>
Date: Sat, 27 Jan 2024 10:31:33 -0500
Subject: [PATCH 5/6] feature/appcheck/final

---
 .../AppCheck/AppCheckApiClientTest.cs         | 369 ++++++++++++------
 .../AppCheck/AppCheckErrorHandlerTest.cs      | 238 +++++++++++
 .../AppCheck/AppCheckTokenFactoryTest.cs      | 267 +++++++++++++
 .../AppCheck/AppCheckTokenGeneratorTest.cs    | 112 ------
 .../AppCheck/AppCheckTokenVerifierTest.cs     | 180 +++++----
 .../AppCheck/CountingAppCheckHandler.cs       |  30 ++
 .../AppCheck/FirebaseAppCheckTests.cs         | 165 ++++++++
 .../AppCheck/MockAppCheckHandler.cs           | 132 +++++++
 .../FirebaseAppCheckTests.cs                  | 198 ----------
 FirebaseAdmin/FirebaseAdmin.sln               |   4 +-
 .../AppCheck/AppCheckApiClient.cs             | 296 +++++++++-----
 .../AppCheck/AppCheckDecodedToken.cs          |  87 +++++
 .../AppCheck/AppCheckErrorCode.cs             |  53 +++
 .../AppCheck/AppCheckErrorHandler.cs          | 229 +++++++++++
 .../FirebaseAdmin/AppCheck/AppCheckToken.cs   |  13 +-
 ...enGenerator.cs => AppCheckTokenFactory.cs} | 133 +++----
 .../AppCheck/AppCheckTokenOptions.cs          |   6 +-
 .../AppCheck/AppCheckTokenVerifier.cs         | 188 +++++++++
 .../AppCheck/AppCheckTokenVerify.cs           | 263 -------------
 .../AppCheck/AppCheckVerifyResponse.cs        |  28 --
 .../AppCheck/AppCheckVerifyTokenOptions.cs    |  26 ++
 .../AppCheck/AppCheckVerifyTokenResponse.cs   |  23 ++
 .../AppCheck/FirebaseAppCheck.cs              | 129 ++++++
 .../AppCheck/FirebaseAppCheckException.cs     |  27 ++
 .../AppCheck/IAppCheckApiClient.cs            |  29 --
 .../AppCheck/IAppCheckTokenGenerator.cs       |  34 --
 .../AppCheck/IAppCheckTokenVerify.cs          |  31 --
 FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs   |  39 --
 .../FirebaseAdmin/AppCheck/KeysRoot.cs        |  16 -
 .../AppCheck/VerifyAppCheckTokenOptions.cs    |  27 --
 .../FirebaseAdmin/Auth/FirebaseToken.cs       |  12 +-
 .../Auth/Jwt/FirebaseTokenFactory.cs          |  55 ---
 .../Auth/Jwt/FirebaseTokenVerifier.cs         |  40 --
 .../Auth/Jwt/HttpPublicKeySource.cs           |  36 --
 .../FirebaseAdmin/FirebaseAppCheck.cs         | 127 ------
 FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs |  12 +
 36 files changed, 2214 insertions(+), 1440 deletions(-)
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckErrorHandlerTest.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenFactoryTest.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/CountingAppCheckHandler.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/FirebaseAppCheckTests.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/MockAppCheckHandler.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckDecodedToken.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorCode.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorHandler.cs
 rename FirebaseAdmin/FirebaseAdmin/AppCheck/{AppCheckTokenGenerator.cs => AppCheckTokenFactory.cs} (60%)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerifier.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenOptions.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenResponse.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheck.cs
 create mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheckException.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/KeysRoot.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/AppCheck/VerifyAppCheckTokenOptions.cs
 delete mode 100644 FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs

diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs
index c6f0b584..9ab0ecdf 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckApiClientTest.cs
@@ -1,211 +1,324 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Net.Http.Headers;
 using System.Text;
 using System.Threading.Tasks;
-using FirebaseAdmin.Check;
+using FirebaseAdmin.Messaging;
+using FirebaseAdmin.Tests;
+using FirebaseAdmin.Tests.AppCheck;
+using FirebaseAdmin.Util;
 using Google.Apis.Auth.OAuth2;
-using Microsoft.VisualStudio.TestPlatform.ObjectModel;
-using Moq;
+using Google.Apis.Http;
+using Newtonsoft.Json;
 using Xunit;
 
-namespace FirebaseAdmin.Tests.AppCheck
+namespace FirebaseAdmin.AppCheck.Tests
 {
     public class AppCheckApiClientTest
     {
+        private static readonly GoogleCredential MockCredential =
+            GoogleCredential.FromFile("./resources/service_account.json");
+
         private readonly string appId = "1:1234:android:1234";
-        private readonly string testTokenToExchange = "signed-custom-token";
-        private readonly string noProjectId = "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.";
 
         [Fact]
-        public void CreateInvalidApp()
+        public void NoProjectId()
         {
-            Assert.Throws<ArgumentException>(() => new AppCheckApiClient(null));
-        }
+            var args = new AppCheckApiClient.Args()
+            {
+                ClientFactory = new HttpClientFactory(),
+                Credential = null,
+            };
 
-        [Fact]
-        public async Task ExchangeTokenNoProjectId()
-        {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            args.ProjectId = null;
+            Assert.Throws<FirebaseAppCheckException>(() => new AppCheckApiClient(args));
 
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException(this.noProjectId));
-            var result = await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
-            Assert.Equal(this.noProjectId, result.Message);
+            args.ProjectId = string.Empty;
+            Assert.Throws<FirebaseAppCheckException>(() => new AppCheckApiClient(args));
         }
 
         [Fact]
-        public async Task ExchangeTokenInvalidAppId()
+        public void NoCredential()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException(this.noProjectId));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, string.Empty));
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, null));
+            var args = new AppCheckApiClient.Args()
+            {
+                ClientFactory = new HttpClientFactory(),
+                Credential = null,
+                ProjectId = "test-project",
+            };
+
+            Assert.Throws<ArgumentNullException>(() => new AppCheckApiClient(args));
         }
 
         [Fact]
-        public async Task ExchangeTokenInvalidCustomTokenAsync()
+        public void NoClientFactory()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException(this.noProjectId));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(string.Empty, this.appId));
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(null, this.appId));
+            var args = new AppCheckApiClient.Args()
+            {
+                ClientFactory = null,
+                Credential = MockCredential,
+                ProjectId = "test-project",
+            };
+
+            Assert.Throws<ArgumentNullException>(() => new AppCheckApiClient(args));
         }
 
         [Fact]
-        public async Task ExchangeTokenFullErrorResponseAsync()
+        public async Task ExchangeToken()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
 
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException("not-found", "Requested entity not found"));
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
 
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
-        }
+            string customToken = "test-token";
 
-        [Fact]
-        public async Task ExchangeTokenErrorCodeAsync()
-        {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            var response = await client.ExchangeTokenAsync(customToken, this.appId);
 
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException("unknown-error", "Unknown server error: {}"));
+            Assert.Equal("test-token", response.Token);
+            Assert.Equal(36000000, response.TtlMillis);
 
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
+            var req = JsonConvert.DeserializeObject<AppCheckApiClient.ExchangeTokenRequest>(handler.LastRequestBody);
+            Assert.Equal("test-token", req.CustomToken);
+            Assert.Equal(1, handler.Calls);
+            this.CheckHeaders(handler.LastRequestHeaders);
         }
 
         [Fact]
-        public async Task ExchangeTokenFullNonJsonAsync()
+        public async Task ExchangeTokenWithEmptyAppId()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException("unknown-error", "Unexpected response with status: 404 and body: not json"));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId));
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
+
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
+
+            string customToken = "test-token";
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await client.ExchangeTokenAsync(customToken, string.Empty));
+
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("appId must be a non-empty string.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
+            Assert.Null(ex.HttpResponse);
+            Assert.Null(ex.InnerException);
+            Assert.Equal(0, handler.Calls);
         }
 
         [Fact]
-        public async Task ExchangeTokenAppErrorAsync()
+        public async Task ExchangeTokenWithNullAppId()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .Throws(new ArgumentException("network-error", "socket hang up"));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.ExchangeTokenAsync(string.Empty, this.appId));
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
+
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
+
+            string customToken = "test-token";
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await client.ExchangeTokenAsync(customToken, null));
+
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("appId must be a non-empty string.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
+            Assert.Null(ex.HttpResponse);
+            Assert.Null(ex.InnerException);
+            Assert.Equal(0, handler.Calls);
         }
 
         [Fact]
-        public async Task ExchangeTokenOnSuccessAsync()
+        public async Task ExchangeTokenWithEmptyCustomToken()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .ReturnsAsync(new AppCheckToken("token", 3000));
-
-            var result = await appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId).ConfigureAwait(false);
-            Assert.NotNull(result);
-            Assert.Equal("token", result.Token);
-            Assert.Equal(3000, result.TtlMillis);
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
+
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await client.ExchangeTokenAsync(string.Empty, this.appId));
+
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("customToken must be a non-empty string.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
+            Assert.Null(ex.HttpResponse);
+            Assert.Null(ex.InnerException);
+            Assert.Equal(0, handler.Calls);
         }
 
         [Fact]
-        public async Task VerifyReplayNoProjectIdAsync()
+        public async Task ExchangeTokenWithNullCustomToken()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException(this.noProjectId));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
+
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await client.ExchangeTokenAsync(null, this.appId));
+
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("customToken must be a non-empty string.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
+            Assert.Null(ex.HttpResponse);
+            Assert.Null(ex.InnerException);
+            Assert.Equal(0, handler.Calls);
         }
 
         [Fact]
-        public async Task VerifyReplayInvaildTokenAsync()
+        public async Task ExchangeTokenWithErrorNoTtlResponse()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException(this.noProjectId));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(string.Empty));
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Token = "test-token",
+                },
+            };
+
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
+
+            string customToken = "test-token";
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await client.ExchangeTokenAsync(customToken, this.appId));
+
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("`ttl` must be a valid duration string with the suffix `s`.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
+            Assert.Null(ex.HttpResponse);
+            Assert.Null(ex.InnerException);
+            Assert.Equal(1, handler.Calls);
         }
 
         [Fact]
-        public async Task VerifyReplayFullErrorAsync()
+        public async Task ExchangeTokenWithErrorNoTokenResponse()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException("not-found", "Requested entity not found"));
-
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.ExchangeTokenResponse()
+                {
+                    Ttl = "36000s",
+                },
+            };
+
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
+
+            string customToken = "test-token";
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await client.ExchangeTokenAsync(customToken, this.appId));
+
+            Assert.Equal(ErrorCode.PermissionDenied, ex.ErrorCode);
+            Assert.Equal("Token is not valid", ex.Message);
+            Assert.Equal(AppCheckErrorCode.AppCheckTokenExpired, ex.AppCheckErrorCode);
+            Assert.Null(ex.HttpResponse);
+            Assert.Null(ex.InnerException);
+            Assert.Equal(1, handler.Calls);
         }
 
         [Fact]
-        public async Task VerifyReplayErrorCodeAsync()
+        public async Task VerifyReplayProtectionWithTrue()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.VerifyTokenResponse()
+                {
+                    AlreadyConsumed = true,
+                },
+            };
 
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException("unknown-error", "Unknown server error: {}"));
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
 
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
-        }
+            string customToken = "test-token";
 
-        [Fact]
-        public async Task VerifyReplayNonJsonAsync()
-        {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            var response = await client.VerifyReplayProtectionAsync(customToken);
 
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException("unknown-error", "Unexpected response with status: 404 and body: not json"));
+            Assert.True(response);
 
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
+            var req = JsonConvert.DeserializeObject<AppCheckApiClient.ExchangeTokenRequest>(handler.LastRequestBody);
+            Assert.Equal(1, handler.Calls);
+            this.CheckHeaders(handler.LastRequestHeaders);
         }
 
         [Fact]
-        public async Task VerifyReplayFirebaseAppErrorAsync()
+        public async Task VerifyReplayProtectionWithFalse()
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            var handler = new MockAppCheckHandler()
+            {
+                Response = new AppCheckApiClient.VerifyTokenResponse()
+                {
+                    AlreadyConsumed = false,
+                },
+            };
 
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException("network-error", "socket hang up"));
+            var factory = new MockHttpClientFactory(handler);
+            var client = this.CreateAppCheckApiClient(factory);
 
-            await Assert.ThrowsAsync<ArgumentException>(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange));
-        }
+            string customToken = "test-token";
 
-        [Fact]
-        public async Task VerifyReplayAlreadyTrueAsync()
-        {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
+            var response = await client.VerifyReplayProtectionAsync(customToken);
 
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .ReturnsAsync(true);
+            Assert.False(response);
 
-            bool res = await appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange).ConfigureAwait(false);
-            Assert.True(res);
+            var req = JsonConvert.DeserializeObject<AppCheckApiClient.ExchangeTokenRequest>(handler.LastRequestBody);
+            Assert.Equal(1, handler.Calls);
+            this.CheckHeaders(handler.LastRequestHeaders);
         }
 
-        [Fact]
-        public async Task VerifyReplayAlreadyFlaseAsync()
+        private AppCheckApiClient CreateAppCheckApiClient(HttpClientFactory factory)
         {
-            var appCheckApiClient = new Mock<IAppCheckApiClient>();
-
-            appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .ReturnsAsync(true);
+            return new AppCheckApiClient(new AppCheckApiClient.Args()
+            {
+                ClientFactory = factory,
+                Credential = MockCredential,
+                ProjectId = "test-project",
+                RetryOptions = RetryOptions.NoBackOff,
+            });
+        }
 
-            bool res = await appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange).ConfigureAwait(false);
-            Assert.True(res);
+        private void CheckHeaders(HttpRequestHeaders header)
+        {
+            var versionHeader = header.GetValues("X-Firebase-Client").First();
+            Assert.Equal(AppCheckApiClient.ClientVersion, versionHeader);
         }
     }
 }
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckErrorHandlerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckErrorHandlerTest.cs
new file mode 100644
index 00000000..d270e2f6
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckErrorHandlerTest.cs
@@ -0,0 +1,238 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using FirebaseAdmin.Util;
+using Xunit;
+
+namespace FirebaseAdmin.AppCheck.Tests
+{
+    public class AppCheckErrorHandlerTest
+    {
+        public static readonly IEnumerable<object[]> AppCheckErrorCodes =
+            new List<object[]>()
+            {
+                new object[]
+                {
+                    "ABORTED",
+                    ErrorCode.Aborted,
+                    AppCheckErrorCode.Aborted,
+                },
+                new object[]
+                {
+                    "INVALID_ARGUMENT",
+                    ErrorCode.InvalidArgument,
+                    AppCheckErrorCode.InvalidArgument,
+                },
+                new object[]
+                {
+                    "INVALID_CREDENTIAL",
+                    ErrorCode.InvalidArgument,
+                    AppCheckErrorCode.InvalidCredential,
+                },
+                new object[]
+                {
+                    "PERMISSION_DENIED",
+                    ErrorCode.PermissionDenied,
+                    AppCheckErrorCode.PermissionDenied,
+                },
+                new object[]
+                {
+                    "UNAUTHENTICATED",
+                    ErrorCode.Unauthenticated,
+                    AppCheckErrorCode.Unauthenticated,
+                },
+                new object[]
+                {
+                    "NOT_FOUND",
+                    ErrorCode.NotFound,
+                    AppCheckErrorCode.NotFound,
+                },
+                new object[]
+                {
+                    "UNKNOWN",
+                    ErrorCode.Unknown,
+                    AppCheckErrorCode.UnknownError,
+                },
+            };
+
+        [Theory]
+        [MemberData(nameof(AppCheckErrorCodes))]
+        public void KnownErrorCode(
+            string code, ErrorCode expectedCode, AppCheckErrorCode expectedAppCheckCode)
+        {
+            var json = $@"{{
+                ""error"": {{
+                    ""message"": ""{code}"",
+                }}
+            }}";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.ServiceUnavailable,
+                Content = new StringContent(json, Encoding.UTF8, "application/json"),
+            };
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpErrorResponse(resp, json);
+
+            Assert.Equal(expectedCode, error.ErrorCode);
+            Assert.Equal(expectedAppCheckCode, error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Null(error.InnerException);
+            Assert.EndsWith($" ({code}).", error.Message);
+        }
+
+        [Theory]
+        [MemberData(nameof(AppCheckErrorCodes))]
+        public void KnownErrorCodeWithDetails(
+            string code, ErrorCode expectedCode, AppCheckErrorCode expectedAppCheckCode)
+        {
+            var json = $@"{{
+                ""error"": {{
+                    ""message"": ""{code}: Some details."",
+                }}
+            }}";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.ServiceUnavailable,
+                Content = new StringContent(json, Encoding.UTF8, "application/json"),
+            };
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpErrorResponse(resp, json);
+
+            Assert.Equal(expectedCode, error.ErrorCode);
+            Assert.Equal(expectedAppCheckCode, error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Null(error.InnerException);
+            Assert.EndsWith($" ({code}).: Some details.", error.Message);
+        }
+
+        [Fact]
+        public void UnknownErrorCode()
+        {
+            var json = $@"{{
+                ""error"": {{
+                    ""message"": ""SOMETHING_UNUSUAL"",
+                }}
+            }}";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.InternalServerError,
+                Content = new StringContent(json, Encoding.UTF8, "application/json"),
+            };
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpErrorResponse(resp, json);
+
+            Assert.Equal(ErrorCode.Internal, error.ErrorCode);
+            Assert.Equal(
+                $"Unexpected HTTP response with status: 500 (InternalServerError){Environment.NewLine}{json}",
+                error.Message);
+            Assert.Null(error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Null(error.InnerException);
+        }
+
+        [Fact]
+        public void UnspecifiedErrorCode()
+        {
+            var json = $@"{{
+                ""error"": {{}}
+            }}";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.InternalServerError,
+                Content = new StringContent(json, Encoding.UTF8, "application/json"),
+            };
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpErrorResponse(resp, json);
+
+            Assert.Equal(ErrorCode.Internal, error.ErrorCode);
+            Assert.Equal(
+                $"Unexpected HTTP response with status: 500 (InternalServerError){Environment.NewLine}{json}",
+                error.Message);
+            Assert.Null(error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Null(error.InnerException);
+        }
+
+        [Fact]
+        public void NoDetails()
+        {
+            var json = @"{}";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.ServiceUnavailable,
+                Content = new StringContent(json, Encoding.UTF8, "application/json"),
+            };
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpErrorResponse(resp, json);
+
+            Assert.Equal(ErrorCode.Unavailable, error.ErrorCode);
+            Assert.Equal(
+                $"Unexpected HTTP response with status: 503 (ServiceUnavailable){Environment.NewLine}{{}}",
+                error.Message);
+            Assert.Null(error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Null(error.InnerException);
+        }
+
+        [Fact]
+        public void NonJson()
+        {
+            var text = "plain text";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.ServiceUnavailable,
+                Content = new StringContent(text, Encoding.UTF8, "text/plain"),
+            };
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpErrorResponse(resp, text);
+
+            Assert.Equal(ErrorCode.Unavailable, error.ErrorCode);
+            Assert.Equal(
+                $"Unexpected HTTP response with status: 503 (ServiceUnavailable){Environment.NewLine}{text}",
+                error.Message);
+            Assert.Null(error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Null(error.InnerException);
+        }
+
+        [Fact]
+        public void DeserializeException()
+        {
+            var text = "plain text";
+            var resp = new HttpResponseMessage()
+            {
+                StatusCode = HttpStatusCode.ServiceUnavailable,
+                Content = new StringContent(text, Encoding.UTF8, "text/plain"),
+            };
+            var inner = new Exception("Deserialization error");
+
+            var error = AppCheckErrorHandler.Instance.HandleDeserializeException(
+                inner, new ResponseInfo(resp, text));
+
+            Assert.Equal(ErrorCode.Unknown, error.ErrorCode);
+            Assert.Equal(
+                $"Error while parsing AppCheck service response. Deserialization error: {text}",
+                error.Message);
+            Assert.Equal(AppCheckErrorCode.UnknownError, error.AppCheckErrorCode);
+            Assert.Same(resp, error.HttpResponse);
+            Assert.Same(inner, error.InnerException);
+        }
+
+        [Fact]
+        public void HttpRequestException()
+        {
+            var exception = new HttpRequestException("network error");
+
+            var error = AppCheckErrorHandler.Instance.HandleHttpRequestException(exception);
+
+            Assert.Equal(ErrorCode.Unknown, error.ErrorCode);
+            Assert.Equal(
+                "Unknown error while making a remote service call: network error", error.Message);
+            Assert.Null(error.AppCheckErrorCode);
+            Assert.Null(error.HttpResponse);
+            Assert.Same(exception, error.InnerException);
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenFactoryTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenFactoryTest.cs
new file mode 100644
index 00000000..4bb10de9
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenFactoryTest.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.AppCheck;
+using FirebaseAdmin.Auth.Jwt;
+using FirebaseAdmin.Auth.Jwt.Tests;
+using FirebaseAdmin.Tests;
+using Google.Apis.Auth;
+using Google.Apis.Auth.OAuth2;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+#pragma warning disable SYSLIB0027
+
+namespace FirebaseAdmin.AppCheck.Tests
+{
+    public class AppCheckTokenFactoryTest
+    {
+        public static readonly IEnumerable<object[]> InvalidStrings = new List<object[]>
+        {
+            new object[] { null },
+            new object[] { string.Empty },
+        };
+
+        private const int ThirtyMinInMs = 1800000;
+        private const int SevenDaysInMs = 604800000;
+        private static readonly MockClock Clock = new MockClock();
+        private static readonly MockSigner Signer = new MockSigner();
+        private readonly string appId = "test-app-id";
+
+        public string Private { get; private set; }
+
+        public string Public { get; private set; }
+
+        [Fact]
+        public async Task CreateCustomToken()
+        {
+            var factory = CreateTokenFactory();
+
+            var token = await factory.CreateCustomTokenAsync("user1");
+
+            MockCustomTokenVerifier.WithTenant().Verify(token);
+        }
+
+        [Theory]
+        [MemberData(nameof(InvalidStrings))]
+        public async Task InvalidAppId(string appId)
+        {
+            var factory = CreateTokenFactory();
+            await Assert.ThrowsAsync<FirebaseAppCheckException>(() => factory.CreateCustomTokenAsync(appId)).ConfigureAwait(false);
+        }
+
+        [Fact]
+        public async Task RejectedOption()
+        {
+            int[] ttls = new int[] { -100, -1, 0, 10, 1799999, 604800001, 1209600000 };
+            foreach (var ttl in ttls)
+            {
+                var option = new AppCheckTokenOptions(ttl);
+
+                var factory = CreateTokenFactory();
+
+                await Assert.ThrowsAsync<FirebaseAppCheckException>(() =>
+                    factory.CreateCustomTokenAsync("user1", option)).ConfigureAwait(false);
+            }
+        }
+
+        [Fact]
+        public async Task FullFilledOption()
+        {
+            int[] ttls = new int[] { -100, -1, 0, 10, 1799999, 604800001, 1209600000 };
+            foreach (var ttl in ttls)
+            {
+                var option = new AppCheckTokenOptions(ttl);
+
+                var factory = CreateTokenFactory();
+
+                await Assert.ThrowsAsync<FirebaseAppCheckException>(() =>
+                    factory.CreateCustomTokenAsync("user1", option)).ConfigureAwait(false);
+            }
+        }
+
+        [Fact]
+        public void Dispose()
+        {
+            FirebaseApp.DeleteAll();
+        }
+
+        [Fact]
+        public async Task DecodedPayload()
+        {
+            List<(int Milliseconds, string ExpectedResult)> ttls = new List<(int, string)>
+            {
+                (ThirtyMinInMs, "1800s"),
+                (ThirtyMinInMs + 1, "1800.001000000s"),
+                (SevenDaysInMs / 2, "302400s"),
+                (SevenDaysInMs - 1, "604799.999000000s"),
+                (SevenDaysInMs, "604800s"),
+            };
+
+            foreach (var value in ttls)
+            {
+                var factory = CreateTokenFactory();
+
+                var option = new AppCheckTokenOptions(value.Milliseconds);
+
+                string token = await factory.CreateCustomTokenAsync(this.appId, option).ConfigureAwait(false);
+                string[] segments = token.Split(".");
+                Assert.Equal(3, segments.Length);
+                var payload = JwtUtils.Decode<AppCheckTokenFactory.CustomTokenPayload>(segments[1]);
+                Assert.Equal(value.ExpectedResult, payload.Ttl);
+            }
+        }
+
+        [Fact]
+        public async Task CorrectHeader()
+        {
+            var factory = CreateTokenFactory();
+
+            var option = new AppCheckTokenOptions(ThirtyMinInMs + 3000);
+
+            string token = await factory.CreateCustomTokenAsync(this.appId, option).ConfigureAwait(false);
+            string[] segments = token.Split(".");
+            Assert.Equal(3, segments.Length);
+            var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
+            Assert.Equal("RS256", header.Algorithm);
+            Assert.Equal("JWT", header.Type);
+        }
+
+        private static AppCheckTokenFactory CreateTokenFactory()
+        {
+            var args = new AppCheckTokenFactory.Args
+            {
+                Signer = Signer,
+                Clock = Clock,
+            };
+            return new AppCheckTokenFactory(args);
+        }
+
+        private abstract class AppCheckCustomTokenVerifier
+        {
+            private readonly string issuer;
+
+            internal AppCheckCustomTokenVerifier(string issuer)
+            {
+                this.issuer = issuer;
+            }
+
+            internal static AppCheckCustomTokenVerifier ForServiceAccount(
+                string clientEmail, byte[] publicKey)
+            {
+                return new RSACustomTokenVerifier(clientEmail, publicKey);
+            }
+
+            internal void Verify(string token, IDictionary<string, object> claims = null)
+            {
+                string[] segments = token.Split(".");
+                Assert.Equal(3, segments.Length);
+
+                var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
+                this.AssertHeader(header);
+
+                var payload = JwtUtils.Decode<AppCheckTokenFactory.CustomTokenPayload>(segments[1]);
+                Assert.Equal(this.issuer, payload.Issuer);
+                Assert.Equal(this.issuer, payload.Subject);
+                Assert.Equal(AppCheckTokenFactory.FirebaseAppCheckAudience, payload.Audience);
+                this.AssertSignature($"{segments[0]}.{segments[1]}", segments[2]);
+            }
+
+            protected virtual void AssertHeader(JsonWebSignature.Header header)
+            {
+                Assert.Equal("RS256", header.Algorithm);
+                Assert.Equal("JWT", header.Type);
+            }
+
+            protected abstract void AssertSignature(string tokenData, string signature);
+
+            private sealed class RSACustomTokenVerifier : AppCheckCustomTokenVerifier
+            {
+                private readonly RSA rsa;
+
+                internal RSACustomTokenVerifier(string issuer, byte[] publicKey)
+                : base(issuer)
+                {
+                    var x509cert = new X509Certificate2(publicKey);
+                    this.rsa = (RSA)x509cert.PublicKey.Key;
+                }
+
+                protected override void AssertSignature(string tokenData, string signature)
+                {
+                    var tokenDataBytes = Encoding.UTF8.GetBytes(tokenData);
+                    var signatureBytes = JwtUtils.Base64DecodeToBytes(signature);
+                    var verified = this.rsa.VerifyData(
+                        tokenDataBytes,
+                        signatureBytes,
+                        HashAlgorithmName.SHA256,
+                        RSASignaturePadding.Pkcs1);
+                    Assert.True(verified);
+                }
+            }
+
+            private sealed class EmulatorCustomTokenVerifier : AppCheckCustomTokenVerifier
+            {
+                internal EmulatorCustomTokenVerifier(string tenantId)
+                : base("firebase-auth-emulator@example.com") { }
+
+                protected override void AssertHeader(JsonWebSignature.Header header)
+                {
+                    Assert.Equal("none", header.Algorithm);
+                    Assert.Equal("JWT", header.Type);
+                }
+
+                protected override void AssertSignature(string tokenData, string signature)
+                {
+                    Assert.Empty(signature);
+                }
+            }
+        }
+
+        private sealed class MockCustomTokenVerifier : AppCheckCustomTokenVerifier
+        {
+            private readonly string expectedSignature;
+
+            private MockCustomTokenVerifier(string issuer, string signature)
+            : base(issuer)
+            {
+                this.expectedSignature = signature;
+            }
+
+            internal static MockCustomTokenVerifier WithTenant()
+            {
+                return new MockCustomTokenVerifier(
+                    MockSigner.KeyIdString, MockSigner.Signature);
+            }
+
+            protected override void AssertSignature(string tokenData, string signature)
+            {
+                Assert.Equal(this.expectedSignature, JwtUtils.Base64Decode(signature));
+            }
+        }
+
+        private sealed class MockSigner : ISigner
+        {
+            public const string KeyIdString = "mock-key-id";
+            public const string Signature = "signature";
+
+            public string Algorithm => JwtUtils.AlgorithmRS256;
+
+            public Task<string> GetKeyIdAsync(CancellationToken cancellationToken)
+            {
+                return Task.FromResult(KeyIdString);
+            }
+
+            public Task<byte[]> SignDataAsync(byte[] data, CancellationToken cancellationToken)
+            {
+                return Task.FromResult(Encoding.UTF8.GetBytes(Signature));
+            }
+
+            public void Dispose() { }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs
deleted file mode 100644
index 96712720..00000000
--- a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenGeneratorTest.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using FirebaseAdmin.Auth.Jwt;
-using FirebaseAdmin.Check;
-using Google.Apis.Auth.OAuth2;
-using Moq;
-using Newtonsoft.Json.Linq;
-using Xunit;
-
-namespace FirebaseAdmin.Tests.AppCheck
-{
-    public class AppCheckTokenGeneratorTest
-    {
-        public static readonly IEnumerable<object[]> InvalidStrings = new List<object[]>
-        {
-            new object[] { null },
-            new object[] { string.Empty },
-        };
-
-        private const int ThirtyMinInMs = 1800000;
-        private const int SevenDaysInMs = 604800000;
-        private static readonly GoogleCredential MockCredential =
-            GoogleCredential.FromAccessToken("test-token");
-
-        private readonly string appId = "test-app-id";
-
-        [Fact]
-        public void ProjectIdFromOptions()
-        {
-            var app = FirebaseApp.Create(new AppOptions()
-            {
-                Credential = MockCredential,
-                ProjectId = "explicit-project-id1",
-            });
-            var verifier = AppCheckTokenVerify.Create(app);
-            Assert.Equal("explicit-project-id1", verifier.ProjectId);
-        }
-
-        [Fact]
-        public void ProjectIdFromServiceAccount()
-        {
-            var app = FirebaseApp.Create(new AppOptions()
-            {
-                Credential = GoogleCredential.FromFile("./resources/service_account.json"),
-            });
-            var verifier = AppCheckTokenVerify.Create(app);
-            Assert.Equal("test-project", verifier.ProjectId);
-        }
-
-        [Fact]
-        public async Task InvalidAppId()
-        {
-            var options = new AppOptions()
-            {
-                Credential = GoogleCredential.FromAccessToken("token"),
-            };
-            var app = FirebaseApp.Create(options, "123");
-
-            AppCheckTokenGenerator tokenGenerator = AppCheckTokenGenerator.Create(app);
-            await Assert.ThrowsAsync<ArgumentException>(() => tokenGenerator.CreateCustomTokenAsync(string.Empty));
-            await Assert.ThrowsAsync<ArgumentException>(() => tokenGenerator.CreateCustomTokenAsync(null));
-        }
-
-        [Fact]
-        public async Task InvalidOptions()
-        {
-            var options = new AppOptions()
-            {
-                Credential = GoogleCredential.FromAccessToken("token"),
-            };
-            var app = FirebaseApp.Create(options, "1234");
-            var tokenGernerator = AppCheckTokenGenerator.Create(app);
-            int[] ttls = new int[] { -100, -1, 0, 10, 1799999, 604800001, 1209600000 };
-            foreach (var ttl in ttls)
-            {
-                var option = new AppCheckTokenOptions(ttl);
-
-                var result = await Assert.ThrowsAsync<ArgumentException>(() =>
-                    tokenGernerator.CreateCustomTokenAsync(this.appId, option));
-            }
-        }
-
-        [Fact]
-        public void ValidOptions()
-        {
-            var options = new AppOptions()
-            {
-                Credential = GoogleCredential.FromAccessToken("token"),
-            };
-            var app = FirebaseApp.Create(options, "12356");
-            var tokenGernerator = AppCheckTokenGenerator.Create(app);
-            int[] ttls = new int[] { ThirtyMinInMs, ThirtyMinInMs + 1, SevenDaysInMs / 2, SevenDaysInMs - 1, SevenDaysInMs };
-            foreach (var ttl in ttls)
-            {
-                var option = new AppCheckTokenOptions(ttl);
-
-                var result = tokenGernerator.CreateCustomTokenAsync(this.appId, option);
-                Assert.NotNull(result);
-            }
-        }
-
-        [Fact]
-        public void Dispose()
-        {
-            FirebaseApp.DeleteAll();
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs
index 80bc34c9..92abfd93 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/AppCheckTokenVerifierTest.cs
@@ -1,17 +1,15 @@
-using System;
+using System;
 using System.Collections.Generic;
-using System.Linq;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using FirebaseAdmin.Auth.Jwt;
 using FirebaseAdmin.Auth.Jwt.Tests;
-using FirebaseAdmin.Check;
-using Google.Apis.Auth.OAuth2;
-using Moq;
+using Google.Apis.Auth;
+using Google.Apis.Util;
+using Newtonsoft.Json;
 using Xunit;
 
-namespace FirebaseAdmin.Tests.AppCheck
+namespace FirebaseAdmin.AppCheck.Tests
 {
     public class AppCheckTokenVerifierTest
     {
@@ -21,118 +19,168 @@ public class AppCheckTokenVerifierTest
             new object[] { string.Empty },
         };
 
-        private static readonly GoogleCredential MockCredential =
-            GoogleCredential.FromAccessToken("test-token");
-
-        [Fact]
-        public void ProjectIdFromOptions()
+        public static readonly IEnumerable<object[]> InvalidTokens = new List<object[]>
         {
-            var app = FirebaseApp.Create(new AppOptions()
-            {
-                Credential = MockCredential,
-                ProjectId = "explicit-project-id",
-            });
-            var verifier = AppCheckTokenVerify.Create(app);
-            Assert.Equal("explicit-project-id", verifier.ProjectId);
-        }
+            new object[] { "TestToken" },
+            new object[] { "Test.Token" },
+            new object[] { "Test.Token.Test.Token" },
+        };
 
-        [Fact]
-        public void ProjectIdFromServiceAccount()
+        public static readonly IEnumerable<object[]> InvalidAudiences = new List<object[]>
         {
-            var app = FirebaseApp.Create(new AppOptions()
-            {
-                Credential = GoogleCredential.FromFile("./resources/service_account.json"),
-            });
-            var verifier = AppCheckTokenVerify.Create(app);
-            Assert.Equal("test-project", verifier.ProjectId);
-        }
+            new object[] { new List<string> { "incorrectAudience" } },
+            new object[] { new List<string> { "12345678", "project_id" } },
+            new object[] { new List<string> { "projects/" + "12345678", "project_id" } },
+        };
 
-        [Theory]
-        [MemberData(nameof(InvalidStrings))]
-        public void InvalidProjectId(string projectId)
+        private readonly string appId = "1:1234:android:1234";
+
+        [Fact]
+        public void NullKeySource()
         {
             var args = FullyPopulatedArgs();
-            args.ProjectId = projectId;
+            args.KeySource = null;
 
-            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+            Assert.Throws<ArgumentNullException>(() => new AppCheckTokenVerifier(args));
         }
 
         [Fact]
-        public void NullKeySource()
+        public void ProjectId()
         {
             var args = FullyPopulatedArgs();
-            args.PublicKeySource = null;
 
-            Assert.Throws<ArgumentNullException>(() => new AppCheckTokenVerify(args));
+            var verifier = new AppCheckTokenVerifier(args);
+
+            Assert.Equal("test-project", verifier.ProjectId);
         }
 
         [Theory]
         [MemberData(nameof(InvalidStrings))]
-        public void InvalidShortName(string shortName)
+        public async Task VerifyWithNullEmptyToken(string token)
         {
             var args = FullyPopulatedArgs();
-            args.ShortName = shortName;
+            var tokenVerifier = new AppCheckTokenVerifier(args);
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await tokenVerifier.VerifyTokenAsync(token));
 
-            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("App Check token must not be null or empty.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
         }
 
         [Theory]
         [MemberData(nameof(InvalidStrings))]
-        public void InvalidIssuer(string issuer)
+        public async Task VerifyWithInvalidProjectId(string projectId)
         {
             var args = FullyPopulatedArgs();
-            args.Issuer = issuer;
+            args.ProjectId = projectId;
+            var tokenVerifier = new AppCheckTokenVerifier(args);
+
+            string token = "test-token";
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await tokenVerifier.VerifyTokenAsync(token));
 
-            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("Must initialize app with a cert credential or set your Firebase project ID as the GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidCredential, ex.AppCheckErrorCode);
         }
 
         [Theory]
-        [MemberData(nameof(InvalidStrings))]
-        public void InvalidOperation(string operation)
+        [MemberData(nameof(InvalidTokens))]
+        public async Task VerifyWithInvalidToken(string token)
         {
             var args = FullyPopulatedArgs();
-            args.Operation = operation;
+            var tokenVerifier = new AppCheckTokenVerifier(args);
+
+            var ex = await Assert.ThrowsAsync<FirebaseAppCheckException>(
+                async () => await tokenVerifier.VerifyTokenAsync(token));
 
-            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+            Assert.Equal(ErrorCode.InvalidArgument, ex.ErrorCode);
+            Assert.Equal("Incorrect number of segments in app check token.", ex.Message);
+            Assert.Equal(AppCheckErrorCode.InvalidArgument, ex.AppCheckErrorCode);
         }
 
         [Theory]
-        [MemberData(nameof(InvalidStrings))]
-        public void InvalidUrl(string url)
+        [MemberData(nameof(InvalidAudiences))]
+        public async Task CheckInvalidAudience(List<string> aud)
         {
+            string token = await this.GeneratorAppCheckTokenAsync(aud).ConfigureAwait(false);
+            string expected = "The provided app check token has incorrect \"aud\" (audience) claim";
             var args = FullyPopulatedArgs();
-            args.Url = url;
-
-            Assert.Throws<ArgumentException>(() => new AppCheckTokenVerify(args));
+            AppCheckTokenVerifier verifier = new AppCheckTokenVerifier(args);
+            var result = await Assert.ThrowsAsync<FirebaseAppCheckException>(() => verifier.VerifyTokenAsync(token)).ConfigureAwait(false);
+            Assert.Contains(expected, result.Message);
         }
 
         [Fact]
-        public void ProjectId()
+        public async Task CheckEmptyAudience()
         {
+            string token = await this.GeneratorAppCheckTokenAsync([]).ConfigureAwait(false);
             var args = FullyPopulatedArgs();
-
-            var verifier = new AppCheckTokenVerify(args);
-
-            Assert.Equal("test-project", verifier.ProjectId);
+            AppCheckTokenVerifier verifier = new AppCheckTokenVerifier(args);
+            var result = await Assert.ThrowsAsync<FirebaseAppCheckException>(() => verifier.VerifyTokenAsync(token)).ConfigureAwait(false);
+            Assert.Equal("Failed to verify app check signature.", result.Message);
         }
 
         [Fact]
-        public void Dispose()
+        public async Task VerifyToken()
         {
-            FirebaseApp.DeleteAll();
+            List<string> aud = new List<string> { "12345678", "projects/test-project" };
+            string token = await this.GeneratorAppCheckTokenAsync(aud).ConfigureAwait(false);
+
+            var args = FullyPopulatedArgs();
+            AppCheckTokenVerifier verifier = new AppCheckTokenVerifier(args);
+            await Assert.ThrowsAsync<FirebaseAppCheckException>(() => verifier.VerifyTokenAsync(token)).ConfigureAwait(false);
         }
 
-        private static FirebaseTokenVerifierArgs FullyPopulatedArgs()
+        private static AppCheckTokenVerifier.Args FullyPopulatedArgs()
         {
-            return new FirebaseTokenVerifierArgs
+            return new AppCheckTokenVerifier.Args
             {
                 ProjectId = "test-project",
-                ShortName = "short name",
-                Operation = "VerifyToken()",
-                Url = "https://firebase.google.com",
-                Issuer = "https://firebase.google.com/",
-                PublicKeySource = JwtTestUtils.DefaultKeySource,
+                Clock = null,
+                KeySource = JwtTestUtils.DefaultKeySource,
             };
         }
+
+        private async Task<string> GeneratorAppCheckTokenAsync(List<string> audience)
+        {
+            DateTime unixEpoch = new DateTime(
+                    1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+            var header = new JsonWebSignature.Header()
+            {
+                Algorithm = "RS256",
+                KeyId = "FGQdnRlzAmKyKr6-Hg_kMQrBkj_H6i6ADnBQz4OI6BU",
+            };
+
+            var signer = EmulatorSigner.Instance;
+            CancellationToken cancellationToken = default;
+            int issued = (int)(SystemClock.Default.UtcNow - unixEpoch).TotalSeconds;
+            var keyId = await signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
+            var payload = new CustomTokenPayload()
+            {
+                Subject = this.appId,
+                Issuer = "https://firebaseappcheck.googleapis.com/" + this.appId,
+                AppId = this.appId,
+                Audience = audience,
+                ExpirationTimeSeconds = 60,
+                IssuedAtTimeSeconds = issued,
+                Ttl = "180000",
+            };
+
+            return await JwtUtils.CreateSignedJwtAsync(
+                header, payload, signer).ConfigureAwait(false);
+        }
+    }
+
+    internal class CustomTokenPayload : JsonWebToken.Payload
+    {
+        [JsonPropertyAttribute("app_id")]
+        public string AppId { get; set; }
+
+        [JsonPropertyAttribute("ttl")]
+        public string Ttl { get; set; }
     }
 }
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/CountingAppCheckHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/CountingAppCheckHandler.cs
new file mode 100644
index 00000000..caf50f1c
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/CountingAppCheckHandler.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FirebaseAdmin.Tests.AppCheck
+{
+    internal abstract class CountingAppCheckHandler : HttpMessageHandler
+    {
+        private int calls;
+
+        public int Calls
+        {
+            get => this.calls;
+        }
+
+        protected sealed override Task<HttpResponseMessage> SendAsync(
+            HttpRequestMessage request, CancellationToken cancellationToken)
+        {
+            var count = Interlocked.Increment(ref this.calls);
+            return this.DoSendAsync(request, count, cancellationToken);
+        }
+
+        protected abstract Task<HttpResponseMessage> DoSendAsync(
+            HttpRequestMessage request, int count, CancellationToken cancellationToken);
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/FirebaseAppCheckTests.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/FirebaseAppCheckTests.cs
new file mode 100644
index 00000000..70139e87
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/FirebaseAppCheckTests.cs
@@ -0,0 +1,165 @@
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.AppCheck;
+using FirebaseAdmin.Auth;
+using FirebaseAdmin.Auth.Jwt;
+using FirebaseAdmin.Tests;
+using FirebaseAdmin.Tests.AppCheck;
+using FirebaseAdmin.Util;
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
+using Moq;
+using Xunit;
+
+namespace FirebaseAdmin.AppCheck.Tests
+{
+    public class FirebaseAppCheckTests : IDisposable
+    {
+        private static readonly GoogleCredential MockCredential = GoogleCredential.FromFile("./resources/service_account.json");
+        private static readonly string ProjectId = "test-project";
+        private static readonly string AppId = "1:1234:android:1234";
+
+        private string noProjectId = "Project ID is required to access app check service. Use a service account "
+                    + "credential or set the project ID explicitly via AppOptions. Alternatively "
+                    + "you can set the project ID via the GOOGLE_CLOUD_PROJECT environment "
+                    + "variable.";
+
+        [Fact]
+        public void GetAppCheckWithoutApp()
+        {
+            Assert.Null(FirebaseAppCheck.DefaultInstance);
+        }
+
+        [Fact]
+        public void GetAppCheckWithoutProjectId()
+        {
+            var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
+            var res = Assert.Throws<FirebaseAppCheckException>(() => new FirebaseAppCheck(app));
+            Assert.Equal(this.noProjectId, res.Message);
+            app.Delete();
+        }
+
+        [Fact]
+        public void GetDefaultAppCheck()
+        {
+            var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential, ProjectId = ProjectId });
+            FirebaseAppCheck appCheck = FirebaseAppCheck.DefaultInstance;
+            Assert.NotNull(appCheck);
+            Assert.Same(appCheck, FirebaseAppCheck.DefaultInstance);
+            app.Delete();
+            Assert.Null(FirebaseAppCheck.DefaultInstance);
+        }
+
+        [Fact]
+        public void GetAppCheck()
+        {
+            var app = FirebaseApp.Create(new AppOptions { Credential = MockCredential, ProjectId = ProjectId }, "MyApp");
+            FirebaseAppCheck appCheck = FirebaseAppCheck.GetAppCheck(app);
+            Assert.NotNull(appCheck);
+            Assert.Same(appCheck, FirebaseAppCheck.GetAppCheck(app));
+            app.Delete();
+            Assert.Throws<InvalidOperationException>(() => FirebaseAppCheck.GetAppCheck(app));
+        }
+
+        [Fact]
+        public async Task GetAppCheckWithApiClientFactory()
+        {
+            var bytes = Encoding.UTF8.GetBytes("signature");
+            var handler = new MockMessageHandler()
+            {
+                Response = new CreatTokenResponse()
+                {
+                    Signature = Convert.ToBase64String(bytes),
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
+            var factory = new MockHttpClientFactory(handler);
+
+            var app = FirebaseApp.Create(
+                new AppOptions()
+                {
+                    Credential = GoogleCredential.FromAccessToken("test-token"),
+                    HttpClientFactory = factory,
+                    ProjectId = ProjectId,
+                },
+                AppId);
+
+            FirebaseAppCheck appCheck = FirebaseAppCheck.GetAppCheck(app);
+            Assert.NotNull(appCheck);
+            Assert.Same(appCheck, FirebaseAppCheck.GetAppCheck(app));
+
+            var response = await appCheck.CreateTokenAsync(app.Name);
+            Assert.Equal("test-token", response.Token);
+            Assert.Equal(36000000, response.TtlMillis);
+        }
+
+        [Fact]
+        public async Task UseAfterDelete()
+        {
+            var app = FirebaseApp.Create(new AppOptions()
+                {
+                    Credential = MockCredential,
+                    ProjectId = ProjectId,
+                });
+
+            FirebaseAppCheck appCheck = FirebaseAppCheck.DefaultInstance;
+            app.Delete();
+            await Assert.ThrowsAsync<ObjectDisposedException>(
+                async () => await appCheck.CreateTokenAsync(AppId));
+        }
+
+        [Fact]
+        public async Task CreateTokenSuccess()
+        {
+            var bytes = Encoding.UTF8.GetBytes("signature");
+            var handler = new MockMessageHandler()
+            {
+                Response = new CreatTokenResponse()
+                {
+                    Signature = Convert.ToBase64String(bytes),
+                    Token = "test-token",
+                    Ttl = "36000s",
+                },
+            };
+            var factory = new MockHttpClientFactory(handler);
+
+            var app = FirebaseApp.Create(
+                new AppOptions()
+                {
+                    Credential = GoogleCredential.FromAccessToken("test-token"),
+                    HttpClientFactory = factory,
+                    ProjectId = ProjectId,
+                },
+                AppId);
+
+            FirebaseAppCheck appCheck = FirebaseAppCheck.GetAppCheck(app);
+
+            var response = await appCheck.CreateTokenAsync(app.Name);
+            Assert.Equal("test-token", response.Token);
+            Assert.Equal(36000000, response.TtlMillis);
+        }
+
+        public void Dispose()
+        {
+            FirebaseApp.DeleteAll();
+        }
+
+        private sealed class CreatTokenResponse
+        {
+            [Newtonsoft.Json.JsonProperty("signedBlob")]
+            public string Signature { get; set; }
+
+            [Newtonsoft.Json.JsonProperty("token")]
+            public string Token { get; set; }
+
+            [Newtonsoft.Json.JsonProperty("ttl")]
+            public string Ttl { get; set; }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/MockAppCheckHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/MockAppCheckHandler.cs
new file mode 100644
index 00000000..5839cb51
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/AppCheck/MockAppCheckHandler.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Google.Apis.Json;
+
+namespace FirebaseAdmin.Tests.AppCheck
+{
+    internal sealed class MockAppCheckHandler : CountingAppCheckHandler
+    {
+        private readonly List<IncomingRequest> requests = new List<IncomingRequest>();
+
+        public MockAppCheckHandler()
+        {
+            this.StatusCode = HttpStatusCode.OK;
+        }
+
+        public delegate void SetHeaders(HttpResponseHeaders respHeaders, HttpContentHeaders contentHeaders);
+
+        public delegate object GetResponse(IncomingRequest request);
+
+        public delegate HttpStatusCode GetStatusCode(IncomingRequest request);
+
+        public IReadOnlyList<IncomingRequest> Requests
+        {
+            get => this.requests;
+        }
+
+        public string LastRequestBody
+        {
+            get => this.requests.LastOrDefault()?.Body;
+        }
+
+        public HttpRequestHeaders LastRequestHeaders
+        {
+            get => this.requests.LastOrDefault()?.Headers;
+        }
+
+        public HttpStatusCode StatusCode { get; set; }
+
+        public object Response { get; set; }
+
+        public Exception Exception { get; set; }
+
+        public SetHeaders ApplyHeaders { get; set; }
+
+        public GetResponse GenerateResponse { get; set; }
+
+        public GetStatusCode GenerateStatusCode { get; set; }
+
+        protected override async Task<HttpResponseMessage> DoSendAsync(
+            HttpRequestMessage request, int count, CancellationToken cancellationToken)
+        {
+            var incomingRequest = await IncomingRequest.CreateAsync(request);
+            this.requests.Add(incomingRequest);
+
+            var tcs = new TaskCompletionSource<HttpResponseMessage>();
+            if (this.Exception != null)
+            {
+                tcs.SetException(this.Exception);
+                return await tcs.Task;
+            }
+
+            if (this.GenerateResponse != null)
+            {
+                this.Response = this.GenerateResponse(incomingRequest);
+            }
+
+            if (this.GenerateStatusCode != null)
+            {
+                this.StatusCode = this.GenerateStatusCode(incomingRequest);
+            }
+
+            string json;
+            if (this.Response is byte[])
+            {
+                json = Encoding.UTF8.GetString(this.Response as byte[]);
+            }
+            else if (this.Response is string)
+            {
+                json = this.Response as string;
+            }
+            else if (this.Response is IList<string>)
+            {
+                json = (this.Response as IList<string>)[count - 1];
+            }
+            else
+            {
+                json = NewtonsoftJsonSerializer.Instance.Serialize(this.Response);
+            }
+
+            var content = new StringContent(json, Encoding.UTF8, "application/json");
+            var resp = new HttpResponseMessage();
+            resp.StatusCode = this.StatusCode;
+            resp.Content = content;
+            if (this.ApplyHeaders != null)
+            {
+                this.ApplyHeaders(resp.Headers, content.Headers);
+            }
+
+            tcs.SetResult(resp);
+            return await tcs.Task;
+        }
+
+        internal sealed class IncomingRequest
+        {
+            internal HttpMethod Method { get; private set; }
+
+            internal Uri Url { get; private set; }
+
+            internal HttpRequestHeaders Headers { get; private set; }
+
+            internal string Body { get; private set; }
+
+            internal static async Task<IncomingRequest> CreateAsync(HttpRequestMessage request)
+            {
+                return new IncomingRequest()
+                {
+                    Method = request.Method,
+                    Url = request.RequestUri,
+                    Headers = request.Headers,
+                    Body = request.Content != null ? await request.Content.ReadAsStringAsync() : null,
+                };
+            }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
deleted file mode 100644
index 7ffa46a0..00000000
--- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppCheckTests.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Net.Http;
-using System.Reflection.Metadata.Ecma335;
-using System.Runtime.CompilerServices;
-using System.Threading;
-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;
-using Newtonsoft.Json.Linq;
-using Xunit;
-
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, " +
-    "PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93" +
-    "bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113b" +
-    "e11e6a7d3113e92484cf7045cc7")]
-
-namespace FirebaseAdmin.Tests
-{
-    public class FirebaseAppCheckTests : IDisposable
-    {
-        private readonly string appId = "1:1234:android:1234";
-        private FirebaseApp mockCredentialApp;
-
-        public FirebaseAppCheckTests()
-        {
-            var credential = GoogleCredential.FromFile("./resources/service_account.json");
-            var options = new AppOptions()
-            {
-                Credential = credential,
-            };
-            this.mockCredentialApp = FirebaseApp.Create(options);
-        }
-
-        [Fact]
-        public void CreateInvalidApp()
-        {
-            Assert.Throws<ArgumentNullException>(() => FirebaseAppCheck.Create(null));
-        }
-
-        [Fact]
-        public void CreateAppCheck()
-        {
-            FirebaseAppCheck withoutAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp);
-            Assert.NotNull(withoutAppIdCreate);
-        }
-
-        [Fact]
-        public void WithoutProjectIDCreate()
-        {
-            // Project ID not set in the environment.
-            Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", null);
-            Environment.SetEnvironmentVariable("GCLOUD_PROJECT", null);
-
-            var options = new AppOptions()
-            {
-                Credential = GoogleCredential.FromAccessToken("token"),
-            };
-            var app = FirebaseApp.Create(options, "1234");
-
-            Assert.Throws<ArgumentException>(() => FirebaseAppCheck.Create(app));
-        }
-
-        [Fact]
-        public void FailedSignCreateToken()
-        {
-            string expected = "sign error";
-            var createTokenMock = new Mock<ISigner>();
-
-            // Setup the mock to throw an exception when SignDataAsync is called
-            createTokenMock.Setup(service => service.SignDataAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
-                           .Throws(new ArgumentException(expected));
-
-            var options = new AppOptions()
-            {
-                Credential = GoogleCredential.FromAccessToken("token"),
-            };
-            var app = FirebaseApp.Create(options, "4321");
-
-            Assert.Throws<ArgumentException>(() => FirebaseAppCheck.Create(app));
-        }
-
-        [Fact]
-        public async Task CreateTokenApiError()
-        {
-            var createTokenMock = new Mock<IAppCheckApiClient>();
-
-            createTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>())).Throws(new ArgumentException("INTERAL_ERROR"));
-
-            FirebaseAppCheck createTokenFromAppId = new FirebaseAppCheck(this.mockCredentialApp);
-
-            await Assert.ThrowsAsync<HttpRequestException>(() => createTokenFromAppId.CreateToken(this.appId));
-        }
-
-        [Fact]
-        public async Task CreateTokenApiErrorOptions()
-        {
-            var createTokenMock = new Mock<IAppCheckApiClient>();
-
-            createTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>())).Throws(new ArgumentException("INTERAL_ERROR"));
-
-            AppCheckTokenOptions options = new (1800000);
-            FirebaseAppCheck createTokenFromAppIdAndTtlMillis = new FirebaseAppCheck(this.mockCredentialApp);
-
-            await Assert.ThrowsAsync<HttpRequestException>(() => createTokenFromAppIdAndTtlMillis.CreateToken(this.appId));
-        }
-
-        [Fact]
-        public async Task CreateTokenAppCheckTokenSuccess()
-        {
-            string createdCustomToken = "custom-token";
-
-            AppCheckTokenGenerator tokenFactory = AppCheckTokenGenerator.Create(this.mockCredentialApp);
-
-            var createCustomTokenMock = new Mock<IAppCheckTokenGenerator>();
-
-            createCustomTokenMock.Setup(service => service.CreateCustomTokenAsync(It.IsAny<string>(), It.IsAny<AppCheckTokenOptions>(), It.IsAny<CancellationToken>()))
-                .ReturnsAsync(createdCustomToken);
-
-            var customRes = await createCustomTokenMock.Object.CreateCustomTokenAsync(this.appId).ConfigureAwait(false);
-            Assert.Equal(createdCustomToken, customRes);
-
-            AppCheckToken expected = new ("token", 3000);
-            var createExchangeTokenMock = new Mock<IAppCheckApiClient>();
-            createExchangeTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny<string>(), It.IsAny<string>()))
-                .ReturnsAsync(expected);
-
-            AppCheckTokenOptions options = new (3000);
-
-            AppCheckToken res = await createExchangeTokenMock.Object.ExchangeTokenAsync("custom-token", this.appId).ConfigureAwait(false);
-            Assert.Equal("token", res.Token);
-            Assert.Equal(3000, res.TtlMillis);
-        }
-
-        [Fact]
-        public async Task VerifyTokenApiError()
-        {
-            var createTokenMock = new Mock<IAppCheckApiClient>();
-            createTokenMock.Setup(service => service.VerifyReplayProtection(It.IsAny<string>()))
-                .Throws(new ArgumentException("INTERAL_ERROR"));
-
-            FirebaseAppCheck verifyToken = new FirebaseAppCheck(this.mockCredentialApp);
-
-            await Assert.ThrowsAsync<ArgumentException>(() => createTokenMock.Object.VerifyReplayProtection("token"));
-        }
-
-        [Fact]
-        public async Task VerifyTokenSuccess()
-        {
-            // Create an instance of FirebaseToken.Args and set its properties.
-            var args = new FirebaseToken.Args
-            {
-                AppId = "1234",
-                Issuer = "issuer",
-                Subject = "subject",
-                Audience = "audience",
-                ExpirationTimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 3600, // 1 hour from now
-                IssuedAtTimeSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
-            };
-            FirebaseToken mockFirebaseToken = new FirebaseToken(args);
-
-            var verifyTokenMock = new Mock<IAppCheckTokenVerify>();
-            verifyTokenMock.Setup(service => service.VerifyTokenAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
-                .ReturnsAsync(mockFirebaseToken);
-
-            var verifyRes = await verifyTokenMock.Object.VerifyTokenAsync(this.appId).ConfigureAwait(false);
-
-            Assert.Equal(verifyRes.AppId, mockFirebaseToken.AppId);
-            Assert.Equal(verifyRes.Issuer, mockFirebaseToken.Issuer);
-            Assert.Equal(verifyRes.Subject, mockFirebaseToken.Subject);
-            Assert.Equal(verifyRes.Audience, mockFirebaseToken.Audience);
-            Assert.Equal(verifyRes.ExpirationTimeSeconds, mockFirebaseToken.ExpirationTimeSeconds);
-            Assert.Equal(verifyRes.IssuedAtTimeSeconds, mockFirebaseToken.IssuedAtTimeSeconds);
-        }
-
-        [Fact]
-        public async Task VerifyTokenInvaild()
-        {
-            FirebaseAppCheck verifyTokenInvaild = new FirebaseAppCheck(this.mockCredentialApp);
-
-            await Assert.ThrowsAsync<ArgumentNullException>(() => verifyTokenInvaild.VerifyToken(null));
-            await Assert.ThrowsAsync<ArgumentNullException>(() => verifyTokenInvaild.VerifyToken(string.Empty));
-        }
-
-        public void Dispose()
-        {
-            FirebaseAppCheck.DeleteAll();
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin.sln b/FirebaseAdmin/FirebaseAdmin.sln
index ab8b6e33..c2efda21 100644
--- a/FirebaseAdmin/FirebaseAdmin.sln
+++ b/FirebaseAdmin/FirebaseAdmin.sln
@@ -1,6 +1,6 @@
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26124.0
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34511.84
 MinimumVisualStudioVersion = 15.0.26124.0
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FirebaseAdmin", "FirebaseAdmin\FirebaseAdmin.csproj", "{20D3B9D9-7461-441A-A798-6B124417F7A3}"
 EndProject
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
index 13e039b4..7b8c3446 100644
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
@@ -1,156 +1,213 @@
 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 FirebaseAdmin.Auth;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using static Google.Apis.Requests.BatchRequest;
+using FirebaseAdmin.Util;
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
+using Google.Apis.Json;
+using Google.Apis.Util;
 
-namespace FirebaseAdmin.Check
+namespace FirebaseAdmin.AppCheck
 {
     /// <summary>
     /// Class that facilitates sending requests to the Firebase App Check backend API.
     /// </summary>
-    /// <returns>A task that completes with the creation of a new App Check token.</returns>
-    /// <exception cref="ArgumentNullException">Thrown if an error occurs while creating the custom token.</exception>
-    /// <value>The Firebase app instance.</value>
-    internal class AppCheckApiClient
+    internal sealed class AppCheckApiClient : IDisposable
     {
-        private const string ApiUrlFormat = "https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken";
+        private const string AppCheckUrlFormat = "https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken";
         private const string OneTimeUseTokenVerificationUrlFormat = "https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}:verifyAppCheckToken";
-        private readonly FirebaseApp app;
-        private string projectId;
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AppCheckApiClient"/> class.
-        /// </summary>
-        /// <param name="value"> Initailize FirebaseApp. </param>
-        public AppCheckApiClient(FirebaseApp value)
+        private readonly ErrorHandlingHttpClient<FirebaseAppCheckException> httpClient;
+        private readonly string projectId;
+
+        internal AppCheckApiClient(Args args)
+        {
+            string noProjectId = "Project ID is required to access app check service. Use a service account "
+                    + "credential or set the project ID explicitly via AppOptions. Alternatively "
+                    + "you can set the project ID via the GOOGLE_CLOUD_PROJECT environment "
+                    + "variable.";
+            if (string.IsNullOrEmpty(args.ProjectId))
+            {
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    noProjectId,
+                    AppCheckErrorCode.InvalidArgument);
+            }
+
+            this.httpClient = new ErrorHandlingHttpClient<FirebaseAppCheckException>(
+                new ErrorHandlingHttpClientArgs<FirebaseAppCheckException>()
+                {
+                    HttpClientFactory = args.ClientFactory.ThrowIfNull(nameof(args.ClientFactory)),
+                    Credential = args.Credential.ThrowIfNull(nameof(args.Credential)),
+                    RequestExceptionHandler = AppCheckErrorHandler.Instance,
+                    ErrorResponseHandler = AppCheckErrorHandler.Instance,
+                    DeserializeExceptionHandler = AppCheckErrorHandler.Instance,
+                    RetryOptions = args.RetryOptions,
+                });
+            this.projectId = args.ProjectId;
+        }
+
+        internal static string ClientVersion
         {
-            if (value == null || value.Options == null)
+            get
             {
-                throw new ArgumentException("Argument passed to admin.appCheck() must be a valid Firebase app instance.");
+                return $"fire-admin-dotnet/{FirebaseApp.GetSdkVersion()}";
             }
+        }
 
-            this.app = value;
-            this.projectId = this.app.Options.ProjectId;
+        public void Dispose()
+        {
+            this.httpClient.Dispose();
         }
 
         /// <summary>
         /// Exchange a signed custom token to App Check token.
         /// </summary>
-        /// <param name="customToken"> The custom token to be exchanged. </param>
-        /// <param name="appId"> The mobile App ID.</param>
-        /// <returns>A <see cref="Task{AppCheckToken}"/> A promise that fulfills with a `AppCheckToken`.</returns>
+        /// <param name="customToken">The custom token to be exchanged.</param>
+        /// <param name="appId">The mobile App ID.</param>
+        /// <returns>A promise that fulfills with a `AppCheckToken`.</returns>
         public async Task<AppCheckToken> ExchangeTokenAsync(string customToken, string appId)
         {
             if (string.IsNullOrEmpty(customToken))
             {
-                throw new ArgumentNullException("First argument passed to customToken must be a valid Firebase app instance.");
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "customToken must be a non-empty string.",
+                    AppCheckErrorCode.InvalidArgument);
             }
 
             if (string.IsNullOrEmpty(appId))
             {
-                throw new ArgumentNullException("Second argument passed to appId must be a valid Firebase app instance.");
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "appId must be a non-empty string.",
+                    AppCheckErrorCode.InvalidArgument);
             }
 
+            var body = new ExchangeTokenRequest()
+            {
+                CustomToken = customToken,
+            };
+
             var url = this.GetUrl(appId);
-            var content = new StringContent(JsonConvert.SerializeObject(new { customToken }), Encoding.UTF8, "application/json");
             var request = new HttpRequestMessage()
             {
                 Method = HttpMethod.Post,
                 RequestUri = new Uri(url),
-                Content = content,
+                Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body),
             };
-            request.Headers.Add("X-Firebase-Client", "fire-admin-node/" + $"{FirebaseApp.GetSdkVersion()}");
-            var httpClient = new HttpClient();
-            var response = await httpClient.SendAsync(request).ConfigureAwait(false);
-            if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
+            AddCommonHeaders(request);
+
+            try
             {
-                var errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-                throw new InvalidOperationException($"BadRequest: {errorContent}");
+                var response = await this.httpClient
+                    .SendAndDeserializeAsync<ExchangeTokenResponse>(request)
+                    .ConfigureAwait(false);
+
+                var appCheck = this.ToAppCheckToken(response.Result);
+
+                return appCheck;
             }
-            else if (!response.IsSuccessStatusCode)
+            catch (HttpRequestException ex)
             {
-                throw new HttpRequestException("network error");
+                throw AppCheckErrorHandler.Instance.HandleHttpRequestException(ex);
             }
-
-            JObject responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
-            string tokenValue = responseData["data"]["token"].ToString();
-            int ttlValue = this.StringToMilliseconds(responseData["data"]["ttl"].ToString());
-            AppCheckToken appCheckToken = new (tokenValue, ttlValue);
-            return appCheckToken;
         }
 
-        /// <summary>
-        /// Exchange a signed custom token to App Check token.
-        /// </summary>
-        /// <param name="token"> The custom token to be exchanged. </param>
-        /// <returns>A alreadyConsumed is true.</returns>
-        public async Task<bool> VerifyReplayProtection(string token)
+        public async Task<bool> VerifyReplayProtectionAsync(string token)
         {
             if (string.IsNullOrEmpty(token))
             {
-                throw new ArgumentException("invalid-argument", "`token` must be a non-empty string.");
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "`tokne` must be a non-empty string.",
+                    AppCheckErrorCode.InvalidArgument);
             }
 
-            string url = this.GetVerifyTokenUrl();
+            var body = new VerifyTokenRequest()
+            {
+                AppCheckToken = token,
+            };
 
+            string url = this.GetVerifyTokenUrl();
             var request = new HttpRequestMessage()
             {
                 Method = HttpMethod.Post,
                 RequestUri = new Uri(url),
-                Content = new StringContent(token),
+                Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body),
             };
+            AddCommonHeaders(request);
+
+            bool ret = false;
 
-            var httpClient = new HttpClient();
-            var response = await httpClient.SendAsync(request).ConfigureAwait(false);
+            try
+            {
+                var response = await this.httpClient
+                    .SendAndDeserializeAsync<VerifyTokenResponse>(request)
+                    .ConfigureAwait(false);
 
-            var responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
-            bool alreadyConsumed = (bool)responseData["data"]["alreadyConsumed"];
-            return alreadyConsumed;
+                ret = response.Result.AlreadyConsumed;
+            }
+            catch (HttpRequestException e)
+            {
+                AppCheckErrorHandler.Instance.HandleHttpRequestException(e);
+            }
+
+            return ret;
         }
 
-        /// <summary>
-        /// Get Verify Token Url .
-        /// </summary>
-        /// <returns>A formatted verify token url.</returns>
-        private string GetVerifyTokenUrl()
+        internal static AppCheckApiClient Create(FirebaseApp app)
         {
-            var urlParams = new Dictionary<string, string>
+            var args = new Args
             {
-                { "projectId", this.projectId },
+                ClientFactory = app.Options.HttpClientFactory,
+                Credential = app.Options.Credential,
+                ProjectId = app.Options.ProjectId,
+                RetryOptions = RetryOptions.Default,
             };
 
-            string baseUrl = this.FormatString(OneTimeUseTokenVerificationUrlFormat, urlParams);
-            return this.FormatString(baseUrl, null);
+            return new AppCheckApiClient(args);
         }
 
-        /// <summary>
-        /// Get url from FirebaseApp Id .
-        /// </summary>
-        /// <param name="appId">The FirebaseApp Id.</param>
-        /// <returns>A formatted verify token url.</returns>
-        private string GetUrl(string appId)
+        private static void AddCommonHeaders(HttpRequestMessage request)
         {
-            if (string.IsNullOrEmpty(this.projectId))
+            request.Headers.Add("X-Firebase-Client", ClientVersion);
+        }
+
+        private AppCheckToken ToAppCheckToken(ExchangeTokenResponse resp)
+        {
+            if (resp == null || string.IsNullOrEmpty(resp.Token))
             {
-                this.projectId = this.app.GetProjectId();
+                throw new FirebaseAppCheckException(
+                    ErrorCode.PermissionDenied,
+                    "Token is not valid",
+                    AppCheckErrorCode.AppCheckTokenExpired);
             }
 
+            if (string.IsNullOrEmpty(resp.Ttl) || !resp.Ttl.EndsWith("s"))
+            {
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "`ttl` must be a valid duration string with the suffix `s`.",
+                    AppCheckErrorCode.InvalidArgument);
+            }
+
+            return new AppCheckToken(resp.Token, this.StringToMilliseconds(resp.Ttl));
+        }
+
+        private string GetUrl(string appId)
+        {
             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);
+
+                throw new FirebaseAppCheckException(
+                    ErrorCode.Unknown,
+                    errorMessage,
+                    AppCheckErrorCode.UnknownError);
             }
 
             var urlParams = new Dictionary<string, string>
@@ -158,41 +215,74 @@ private string GetUrl(string appId)
                 { "projectId", this.projectId },
                 { "appId", appId },
             };
-            string baseUrl = this.FormatString(ApiUrlFormat, urlParams);
-            return baseUrl;
+
+            return HttpUtils.FormatString(AppCheckUrlFormat, urlParams);
         }
 
-        /// <summary>
-        /// Converts a duration string with the suffix `s` to milliseconds.
-        /// </summary>
-        /// <param name="duration">The duration as a string with the suffix "s" preceded by the number of seconds.</param>
-        /// <returns> The duration in milliseconds.</returns>
         private int StringToMilliseconds(string duration)
         {
-            if (string.IsNullOrEmpty(duration) || !duration.EndsWith("s"))
-            {
-                throw new ArgumentException("invalid-argument", "`ttl` must be a valid duration string with the suffix `s`.");
-            }
-
             string modifiedString = duration.Remove(duration.Length - 1);
             return int.Parse(modifiedString) * 1000;
         }
 
-        /// <summary>
-        /// Formats a string of form 'project/{projectId}/{api}' and replaces with corresponding arguments {projectId: '1234', api: 'resource'}.
-        /// </summary>
-        /// <param name="str">The original string where the param need to be replaced.</param>
-        /// <param name="urlParams">The optional parameters to replace in thestring.</param>
-        /// <returns> The resulting formatted string. </returns>
-        private string FormatString(string str, Dictionary<string, string> urlParams)
+        private string GetVerifyTokenUrl()
         {
-            string formatted = str;
-            foreach (var key in urlParams.Keys)
+            if (string.IsNullOrEmpty(this.projectId))
             {
-                formatted = Regex.Replace(formatted, $"{{{key}}}", urlParams[key]);
+                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 FirebaseAppCheckException(
+                    ErrorCode.Unknown,
+                    errorMessage,
+                    AppCheckErrorCode.UnknownError);
             }
 
-            return formatted;
+            var urlParams = new Dictionary<string, string>
+            {
+                { "projectId", this.projectId },
+            };
+
+            return HttpUtils.FormatString(OneTimeUseTokenVerificationUrlFormat, urlParams);
+        }
+
+        internal sealed class Args
+        {
+            internal HttpClientFactory ClientFactory { get; set; }
+
+            internal GoogleCredential Credential { get; set; }
+
+            internal string ProjectId { get; set; }
+
+            internal RetryOptions RetryOptions { get; set; }
+        }
+
+        internal class ExchangeTokenRequest
+        {
+            [Newtonsoft.Json.JsonProperty("customToken")]
+            public string CustomToken { get; set; }
+        }
+
+        internal class ExchangeTokenResponse
+        {
+            [Newtonsoft.Json.JsonProperty("token")]
+            public string Token { get; set; }
+
+            [Newtonsoft.Json.JsonProperty("ttl")]
+            public string Ttl { get; set; }
+        }
+
+        internal class VerifyTokenRequest
+        {
+            [Newtonsoft.Json.JsonProperty("appCheckToken")]
+            public string AppCheckToken { get; set; }
+        }
+
+        internal class VerifyTokenResponse
+        {
+            [Newtonsoft.Json.JsonProperty("alreadyConsumed")]
+            public bool AlreadyConsumed { get; set; }
         }
     }
 }
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckDecodedToken.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckDecodedToken.cs
new file mode 100644
index 00000000..63c15eba
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckDecodedToken.cs
@@ -0,0 +1,87 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Interface representing a decoded Firebase App Check token, returned from the {@link AppCheck.verifyToken} method..
+    /// </summary>
+    public class AppCheckDecodedToken
+    {
+        internal AppCheckDecodedToken(Args args)
+        {
+            this.AppId = args.AppId;
+            this.Issuer = args.Issuer;
+            this.Subject = args.Subject;
+            this.Audience = args.Audience;
+            this.ExpirationTimeSeconds = (int)args.ExpirationTimeSeconds;
+            this.IssuedAtTimeSeconds = (int)args.IssuedAtTimeSeconds;
+        }
+
+        /// <summary>
+        /// Gets or sets the issuer identifier for the issuer of the response.
+        /// </summary>
+        public string Issuer { get; set; }
+
+        /// <summary>
+        /// Gets or sets the Firebase App ID corresponding to the app the token belonged to.
+        /// As a convenience, this value is copied over to the {@link AppCheckDecodedToken.app_id | app_id} property.
+        /// </summary>
+        public string Subject { get; set; }
+
+        /// <summary>
+        /// Gets or sets the audience for which this token is intended.
+        /// This value is a JSON array of two strings, the first is the project number of your
+        /// Firebase project, and the second is the project ID of the same project.
+        /// </summary>
+        public string[] Audience { get; set; }
+
+        /// <summary>
+        /// Gets or sets the App Check token's c time, in seconds since the Unix epoch. That is, the
+        /// time at which this App Check token expires and should no longer be considered valid.
+        /// </summary>
+        public int ExpirationTimeSeconds { get; set; }
+
+        /// <summary>
+        /// Gets or sets the App Check token's issued-at time, in seconds since the Unix epoch. That is, the
+        /// time at which this App Check token was issued and should start to be considered valid.
+        /// </summary>
+        public int IssuedAtTimeSeconds { get; set; }
+
+        /// <summary>
+        /// Gets or sets the App ID corresponding to the App the App Check token belonged to.
+        /// This value is not actually one of the JWT token claims. It is added as a
+        /// convenience, and is set as the value of the {@link AppCheckDecodedToken.sub | sub} property.
+        /// </summary>
+        public string AppId { get; set; }
+
+        /// <summary>
+        /// Gets or sets key .
+        /// </summary>
+        public Dictionary<string, string> Key { get; set; }
+        ////[key: string]: any;
+
+        internal sealed class Args
+        {
+            public string AppId { get; internal set; }
+
+            [JsonProperty("app_id")]
+            internal string Issuer { get; set; }
+
+            [JsonProperty("sub")]
+            internal string Subject { get; set; }
+
+            [JsonProperty("aud")]
+            internal string[] Audience { get; set; }
+
+            [JsonProperty("exp")]
+            internal long ExpirationTimeSeconds { get; set; }
+
+            [JsonProperty("iat")]
+            internal long IssuedAtTimeSeconds { get; set; }
+
+            [JsonIgnore]
+            internal IReadOnlyDictionary<string, object> Claims { get; set; }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorCode.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorCode.cs
new file mode 100644
index 00000000..32b6645b
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorCode.cs
@@ -0,0 +1,53 @@
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Error codes that can be raised by the Firebase App Check APIs.
+    /// </summary>
+    public enum AppCheckErrorCode
+    {
+        /// <summary>
+        /// Process is aborted
+        /// </summary>
+        Aborted,
+
+        /// <summary>
+        /// Argument is not valid
+        /// </summary>
+        InvalidArgument,
+
+        /// <summary>
+        /// Credential is not valid
+        /// </summary>
+        InvalidCredential,
+
+        /// <summary>
+        /// The server internal error
+        /// </summary>
+        InternalError,
+
+        /// <summary>
+        /// Permission is denied
+        /// </summary>
+        PermissionDenied,
+
+        /// <summary>
+        /// Unauthenticated
+        /// </summary>
+        Unauthenticated,
+
+        /// <summary>
+        /// Resource is not found
+        /// </summary>
+        NotFound,
+
+        /// <summary>
+        /// App Check Token is expired
+        /// </summary>
+        AppCheckTokenExpired,
+
+        /// <summary>
+        /// Unknown Error
+        /// </summary>
+        UnknownError,
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorHandler.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorHandler.cs
new file mode 100644
index 00000000..7fe1f7c0
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckErrorHandler.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using FirebaseAdmin.Util;
+using Google.Apis.Json;
+using Newtonsoft.Json;
+
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Parses error responses received from the Auth service, and creates instances of
+    /// <see cref="FirebaseAppCheckException"/>.
+    /// </summary>
+    internal sealed class AppCheckErrorHandler
+    : HttpErrorHandler<FirebaseAppCheckException>,
+        IHttpRequestExceptionHandler<FirebaseAppCheckException>,
+        IDeserializeExceptionHandler<FirebaseAppCheckException>
+    {
+        internal static readonly AppCheckErrorHandler Instance = new AppCheckErrorHandler();
+
+        private static readonly IReadOnlyDictionary<string, ErrorInfo> CodeToErrorInfo =
+            new Dictionary<string, ErrorInfo>()
+            {
+                {
+                    "ABORTED",
+                    new ErrorInfo(
+                        ErrorCode.Aborted,
+                        AppCheckErrorCode.Aborted,
+                        "App check is aborted")
+                },
+                {
+                    "INVALID_ARGUMENT",
+                    new ErrorInfo(
+                        ErrorCode.InvalidArgument,
+                        AppCheckErrorCode.InvalidArgument,
+                        "An argument is not valid")
+                },
+                {
+                    "INVALID_CREDENTIAL",
+                    new ErrorInfo(
+                        ErrorCode.InvalidArgument,
+                        AppCheckErrorCode.InvalidCredential,
+                        "The credential is not valid")
+                },
+                {
+                    "PERMISSION_DENIED",
+                    new ErrorInfo(
+                        ErrorCode.PermissionDenied,
+                        AppCheckErrorCode.PermissionDenied,
+                        "The permission is denied")
+                },
+                {
+                    "UNAUTHENTICATED",
+                    new ErrorInfo(
+                        ErrorCode.Unauthenticated,
+                        AppCheckErrorCode.Unauthenticated,
+                        "Unauthenticated")
+                },
+                {
+                    "NOT_FOUND",
+                    new ErrorInfo(
+                        ErrorCode.NotFound,
+                        AppCheckErrorCode.NotFound,
+                        "The resource is not found")
+                },
+                {
+                    "UNKNOWN",
+                    new ErrorInfo(
+                        ErrorCode.Unknown,
+                        AppCheckErrorCode.UnknownError,
+                        "unknown-error")
+                },
+            };
+
+        private AppCheckErrorHandler() { }
+
+        public FirebaseAppCheckException HandleHttpRequestException(
+            HttpRequestException exception)
+        {
+            var temp = exception.ToFirebaseException();
+            return new FirebaseAppCheckException(
+                temp.ErrorCode,
+                temp.Message,
+                inner: temp.InnerException,
+                response: temp.HttpResponse);
+        }
+
+        public FirebaseAppCheckException HandleDeserializeException(
+            Exception exception, ResponseInfo responseInfo)
+        {
+            return new FirebaseAppCheckException(
+                ErrorCode.Unknown,
+                $"Error while parsing AppCheck service response. Deserialization error: {responseInfo.Body}",
+                AppCheckErrorCode.UnknownError,
+                inner: exception,
+                response: responseInfo.HttpResponse);
+        }
+
+        protected sealed override FirebaseExceptionArgs CreateExceptionArgs(
+            HttpResponseMessage response, string body)
+        {
+            var appCheckError = this.ParseAppCheckError(body);
+
+            ErrorInfo info;
+            CodeToErrorInfo.TryGetValue(appCheckError.Code, out info);
+
+            var defaults = base.CreateExceptionArgs(response, body);
+            return new FirebaseAppCheckExceptionArgs()
+            {
+                Code = info?.ErrorCode ?? defaults.Code,
+                Message = info?.GetMessage(appCheckError) ?? defaults.Message,
+                HttpResponse = response,
+                ResponseBody = body,
+                AppCheckErrorCode = info?.AppCheckErrorCode,
+            };
+        }
+
+        protected override FirebaseAppCheckException CreateException(FirebaseExceptionArgs args)
+        {
+            return new FirebaseAppCheckException(
+                args.Code,
+                args.Message,
+                (args as FirebaseAppCheckExceptionArgs).AppCheckErrorCode,
+                response: args.HttpResponse);
+        }
+
+        private AppCheckError ParseAppCheckError(string body)
+        {
+            try
+            {
+                var parsed = NewtonsoftJsonSerializer.Instance.Deserialize<AppCheckErrorResponse>(body);
+                return parsed.Error ?? new AppCheckError();
+            }
+            catch
+            {
+                // Ignore any error that may occur while parsing the error response. The server
+                // may have responded with a non-json body.
+                return new AppCheckError();
+            }
+        }
+
+        /// <summary>
+        /// Describes a class of errors that can be raised by the Firebase Auth backend API.
+        /// </summary>
+        private sealed class ErrorInfo
+        {
+            private readonly string message;
+
+            internal ErrorInfo(ErrorCode code, AppCheckErrorCode appCheckErrorCode, string message)
+            {
+                this.ErrorCode = code;
+                this.AppCheckErrorCode = appCheckErrorCode;
+                this.message = message;
+            }
+
+            internal ErrorCode ErrorCode { get; private set; }
+
+            internal AppCheckErrorCode AppCheckErrorCode { get; private set; }
+
+            internal string GetMessage(AppCheckError appCheckError)
+            {
+                var message = $"{this.message} ({appCheckError.Code}).";
+                if (!string.IsNullOrEmpty(appCheckError.Detail))
+                {
+                    return $"{message}: {appCheckError.Detail}";
+                }
+
+                return $"{message}";
+            }
+        }
+
+        private sealed class FirebaseAppCheckExceptionArgs : FirebaseExceptionArgs
+        {
+            internal AppCheckErrorCode? AppCheckErrorCode { get; set; }
+        }
+
+        private sealed class AppCheckError
+        {
+            [JsonProperty("message")]
+            internal string Message { get; set; }
+
+            /// <summary>
+            /// Gets the Firebase Auth error code extracted from the response. Returns empty string
+            /// if the error code cannot be determined.
+            /// </summary>
+            internal string Code
+            {
+                get
+                {
+                    var separator = this.GetSeparator();
+                    if (separator != -1)
+                    {
+                        return this.Message.Substring(0, separator);
+                    }
+
+                    return this.Message ?? string.Empty;
+                }
+            }
+
+            /// <summary>
+            /// Gets the error detail sent by the Firebase Auth API. May be null.
+            /// </summary>
+            internal string Detail
+            {
+                get
+                {
+                    var separator = this.GetSeparator();
+                    if (separator != -1)
+                    {
+                        return this.Message.Substring(separator + 1).Trim();
+                    }
+
+                    return null;
+                }
+            }
+
+            private int GetSeparator()
+            {
+                return this.Message?.IndexOf(':') ?? -1;
+            }
+        }
+
+        private sealed class AppCheckErrorResponse
+        {
+            [JsonProperty("error")]
+            internal AppCheckError Error { get; set; }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckToken.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckToken.cs
index 1441631c..deb2abd6 100644
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckToken.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckToken.cs
@@ -1,23 +1,16 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace FirebaseAdmin.Check
+namespace FirebaseAdmin.AppCheck
 {
     /// <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.
+        /// Gets or sets the Firebase App Check token.
         /// </summary>
-        public string Token { get; } = tokenValue;
+        public string Token { get; set; } = tokenValue;
 
         /// <summary>
         /// Gets or sets the time-to-live duration of the token in milliseconds.
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenFactory.cs
similarity index 60%
rename from FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs
rename to FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenFactory.cs
index 6c0e7bf1..c2f6db25 100644
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenGenerator.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenFactory.cs
@@ -1,34 +1,20 @@
 using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using FirebaseAdmin.Auth;
 using FirebaseAdmin.Auth.Jwt;
+using FirebaseAdmin.Messaging.Util;
 using Google.Apis.Auth;
 using Google.Apis.Util;
 using Newtonsoft.Json;
 
-[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" +
-"002400000480000094000000060200000024000052534131000400000100010081328559eaab41" +
-"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02" +
-"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597" +
-"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f" +
-"badddb9c")]
-
-namespace FirebaseAdmin.Check
+namespace FirebaseAdmin.AppCheck
 {
     /// <summary>
     /// A helper class that creates Firebase custom tokens.
     /// </summary>
-    internal class AppCheckTokenGenerator
+    internal class AppCheckTokenFactory : IDisposable
     {
-        public const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
-           + "google.identity.identitytoolkit.v1.IdentityToolkit";
-
         public const string FirebaseAppCheckAudience = "https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService";
 
         public const int OneMinuteInSeconds = 60;
@@ -38,25 +24,7 @@ internal class AppCheckTokenGenerator
         public static readonly DateTime UnixEpoch = new DateTime(
             1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
 
-        public static readonly ImmutableList<string> ReservedClaims = ImmutableList.Create(
-            "acr",
-            "amr",
-            "at_hash",
-            "aud",
-            "auth_time",
-            "azp",
-            "cnf",
-            "c_hash",
-            "exp",
-            "firebase",
-            "iat",
-            "iss",
-            "jti",
-            "nbf",
-            "nonce",
-            "sub");
-
-        internal AppCheckTokenGenerator(Args args)
+        internal AppCheckTokenFactory(Args args)
         {
             args.ThrowIfNull(nameof(args));
 
@@ -70,8 +38,6 @@ internal AppCheckTokenGenerator(Args args)
 
         internal IClock Clock { get; }
 
-        internal string TenantId { get; }
-
         internal bool IsEmulatorMode { get; }
 
         public void Dispose()
@@ -79,54 +45,30 @@ public void Dispose()
             this.Signer.Dispose();
         }
 
-        internal static AppCheckTokenGenerator Create(FirebaseApp app)
-        {
-            ISigner signer = null;
-            var serviceAccount = app.Options.Credential.ToServiceAccountCredential();
-            if (serviceAccount != null)
-            {
-                // If the app was initialized with a service account, use it to sign
-                // tokens locally.
-                signer = new ServiceAccountSigner(serviceAccount);
-            }
-            else if (string.IsNullOrEmpty(app.Options.ServiceAccountId))
-            {
-                // If no service account ID is specified, attempt to discover one and invoke the
-                // IAM service with it.
-                signer = IAMSigner.Create(app);
-            }
-            else
-            {
-                // If a service account ID is specified, invoke the IAM service with it.
-                signer = FixedAccountIAMSigner.Create(app);
-            }
-
-            var args = new Args
-            {
-                Signer = signer,
-                IsEmulatorMode = Utils.IsEmulatorModeFromEnvironment,
-            };
-            return new AppCheckTokenGenerator(args);
-        }
-
-        internal async Task<string> CreateCustomTokenAsync(
+        /// <summary>
+        /// Creates a new custom token that can be exchanged to an App Check token.
+        /// </summary>
+        /// <param name="appId">The mobile App ID.</param>
+        /// <param name="options">Options for AppCheckToken with ttl. Possibly null.</param>
+        /// <param name="cancellationToken">A cancellation token to monitor the asynchronous.</param>
+        /// <returns>A Promise fulfilled with a custom token signed with a service account key that can be exchanged to an App Check token.</returns>
+        public async Task<string> CreateCustomTokenAsync(
             string appId,
             AppCheckTokenOptions options = null,
             CancellationToken cancellationToken = default)
         {
             if (string.IsNullOrEmpty(appId))
             {
-                throw new ArgumentException("appId must not be null or empty");
-            }
-            else if (appId.Length > 128)
-            {
-                throw new ArgumentException("appId must not be longer than 128 characters");
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "`appId` must be a non-empty string.",
+                    AppCheckErrorCode.InvalidArgument);
             }
 
             string customOptions = " ";
             if (options != null)
             {
-                customOptions = this.ValidateTokenOptions(options).ToString();
+                customOptions = this.ValidateTokenOptions(options);
             }
 
             var header = new JsonWebSignature.Header()
@@ -152,19 +94,50 @@ internal async Task<string> CreateCustomTokenAsync(
                 header, payload, this.Signer).ConfigureAwait(false);
         }
 
-        private int ValidateTokenOptions(AppCheckTokenOptions options)
+        internal static AppCheckTokenFactory Create(FirebaseApp app)
+        {
+            ISigner signer = null;
+            var serviceAccount = app.Options.Credential.ToServiceAccountCredential();
+            if (serviceAccount != null)
+            {
+                signer = new ServiceAccountSigner(serviceAccount);
+            }
+            else if (string.IsNullOrEmpty(app.Options.ServiceAccountId))
+            {
+                signer = IAMSigner.Create(app);
+            }
+            else
+            {
+                signer = FixedAccountIAMSigner.Create(app);
+            }
+
+            var args = new Args
+            {
+                Signer = signer,
+                IsEmulatorMode = Utils.IsEmulatorModeFromEnvironment,
+            };
+            return new AppCheckTokenFactory(args);
+        }
+
+        private string ValidateTokenOptions(AppCheckTokenOptions options)
         {
             if (options == null)
             {
-                throw new ArgumentException("invalid-argument", "AppCheckTokenOptions must be a non-null object.");
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "AppCheckTokenOptions must be a non-null object.",
+                    AppCheckErrorCode.InvalidArgument);
             }
 
             if (options.TtlMillis < (OneMinuteInMills * 30) || options.TtlMillis > (OneDayInMills * 7))
             {
-                throw new ArgumentException("invalid-argument", "ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).");
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).",
+                    AppCheckErrorCode.InvalidArgument);
             }
 
-            return options.TtlMillis;
+            return TimeConverter.LongMillisToString(options.TtlMillis);
         }
 
         internal class CustomTokenPayload : JsonWebToken.Payload
@@ -182,8 +155,6 @@ internal sealed class Args
 
             internal IClock Clock { get; set; }
 
-            internal string TenantId { get; set; }
-
             internal bool IsEmulatorMode { get; set; }
         }
     }
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenOptions.cs
index 453f9c5e..2f4cd730 100644
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenOptions.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenOptions.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace FirebaseAdmin.Auth.Jwt
+namespace FirebaseAdmin.AppCheck
 {
     /// <summary>
     /// Representing App Check token options.
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerifier.cs
new file mode 100644
index 00000000..7c108a03
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerifier.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Auth.Jwt;
+using Google.Apis.Auth;
+using Google.Apis.Util;
+
+namespace FirebaseAdmin.AppCheck
+{
+    internal class AppCheckTokenVerifier
+    {
+        private const string AppCheckIssuer = "https://firebaseappcheck.googleapis.com/";
+        private const string JWKSURL = "https://firebaseappcheck.googleapis.com/v1/jwks";
+
+        private static readonly IReadOnlyList<string> StandardClaims =
+            ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
+
+        internal AppCheckTokenVerifier(Args args)
+        {
+            args.ThrowIfNull(nameof(args));
+            this.ProjectId = args.ProjectId;
+            this.KeySource = args.KeySource.ThrowIfNull(nameof(args.KeySource));
+            this.Clock = args.Clock ?? SystemClock.Default;
+        }
+
+        internal IClock Clock { get; }
+
+        internal string ProjectId { get; }
+
+        internal IPublicKeySource KeySource { get; }
+
+        /// <summary>
+        /// Verifies the format and signature of a Firebase App Check token.
+        /// </summary>
+        /// <param name="token">The Firebase Auth JWT token to verify.</param>
+        /// <param name="cancellationToken">A cancellation token to monitor the asynchronous operation.</param>
+        /// <returns>A task that completes with a <see cref="AppCheckDecodedToken"/> representing
+        /// a user with the specified user ID.</returns>
+        public async Task<AppCheckDecodedToken> VerifyTokenAsync(
+            string token, CancellationToken cancellationToken = default(CancellationToken))
+        {
+            if (string.IsNullOrEmpty(token))
+            {
+                throw new FirebaseAppCheckException(
+                      ErrorCode.InvalidArgument,
+                      "App Check token must not be null or empty.",
+                      AppCheckErrorCode.InvalidArgument);
+            }
+
+            if (string.IsNullOrEmpty(this.ProjectId))
+            {
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "Must initialize app with a cert credential or set your Firebase project ID as the GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.",
+                    AppCheckErrorCode.InvalidCredential);
+            }
+
+            string[] segments = token.Split('.');
+            if (segments.Length != 3)
+            {
+                throw new FirebaseAppCheckException(
+                      ErrorCode.InvalidArgument,
+                      "Incorrect number of segments in app check token.",
+                      AppCheckErrorCode.InvalidArgument);
+            }
+
+            var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
+            var payload = JwtUtils.Decode<AppCheckDecodedToken.Args>(segments[1]);
+
+            var projectIdMessage = $"Incorrect number of segments in app check Token."
+                + "project as the credential used to initialize this SDK.";
+            var scopedProjectId = $"projects/{this.ProjectId}";
+            string errorMessage = string.Empty;
+
+            if (header.Algorithm != "RS256")
+            {
+                errorMessage = "The provided app check token has incorrect algorithm. Expected 'RS256'" +
+                " but got " + $"{header.Algorithm}" + ".";
+            }
+            else if (payload.Audience.Length > 0 || payload.Audience.Contains(scopedProjectId))
+            {
+                errorMessage = "The provided app check token has incorrect \"aud\" (audience) claim. Expected " +
+                    scopedProjectId + "but got" + payload.Audience + "." + projectIdMessage;
+            }
+            else if (payload.Issuer.StartsWith(AppCheckIssuer))
+            {
+                errorMessage = $"The provided app check token has incorrect \"iss\" (issuer) claim.";
+            }
+            else if (payload.Subject == null)
+            {
+                errorMessage = "The provided app check token has no \"sub\" (subject) claim.";
+            }
+            else if (payload.Subject == string.Empty)
+            {
+                errorMessage = "The provided app check token has an empty string \"sub\" (subject) claim.";
+            }
+
+            if (!string.IsNullOrEmpty(errorMessage))
+            {
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    errorMessage,
+                    AppCheckErrorCode.InvalidArgument);
+            }
+
+            await this.VerifySignatureAsync(segments, header.KeyId, cancellationToken)
+                .ConfigureAwait(false);
+            var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
+
+            // Remove standard claims, so that only custom claims would remain.
+            foreach (var claim in StandardClaims)
+            {
+                allClaims.Remove(claim);
+            }
+
+            payload.Claims = allClaims.ToImmutableDictionary();
+            return new AppCheckDecodedToken(payload);
+        }
+
+        internal static AppCheckTokenVerifier Create(FirebaseApp app)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            var projectId = app.GetProjectId();
+            if (string.IsNullOrEmpty(projectId))
+            {
+                throw new ArgumentException(
+                    "Must initialize FirebaseApp with a project ID to verify session cookies.");
+            }
+
+            IPublicKeySource keySource = new HttpPublicKeySource(
+                JWKSURL, SystemClock.Default, app.Options.HttpClientFactory);
+
+            var args = new Args
+            {
+                ProjectId = projectId,
+                KeySource = keySource,
+            };
+            return new AppCheckTokenVerifier(args);
+        }
+
+        /// <summary>
+        /// Verifies the integrity of a JWT by validating its signature. The JWT must be specified
+        /// as an array of three segments (header, body and signature).
+        /// </summary>
+        private async Task VerifySignatureAsync(
+            string[] segments, string keyId, CancellationToken cancellationToken)
+        {
+            byte[] hash;
+            using (var hashAlg = SHA256.Create())
+            {
+                hash = hashAlg.ComputeHash(
+                    Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
+            }
+
+            var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
+            var keys = await this.KeySource.GetPublicKeysAsync(cancellationToken)
+                .ConfigureAwait(false);
+            var verified = keys.Any(key =>
+                key.Id == keyId && key.RSA.VerifyHash(
+                    hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
+            if (!verified)
+            {
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    "Failed to verify app check signature.",
+                    AppCheckErrorCode.InvalidCredential);
+            }
+        }
+
+        internal sealed class Args
+        {
+            internal IClock Clock { get; set; }
+
+            internal string ProjectId { get; set; }
+
+            internal IPublicKeySource KeySource { get; set; }
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs
deleted file mode 100644
index b11334b3..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckTokenVerify.cs
+++ /dev/null
@@ -1,263 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using FirebaseAdmin.Auth;
-using FirebaseAdmin.Auth.Jwt;
-using Google.Apis.Auth;
-using Google.Apis.Util;
-
-namespace FirebaseAdmin.Check
-{
-    internal class AppCheckTokenVerify
-    {
-        /*private const string IdTokenCertUrl = "https://www.googleapis.com/robot/v1/metadata/x509/"
-            + "securetoken@system.gserviceaccount.com";
-
-        private const string SessionCookieCertUrl = "https://www.googleapis.com/identitytoolkit/v3/"
-            + "relyingparty/publicKeys";
-*/
-        private const string AppCheckIuuser = "https://firebaseappcheck.googleapis.com/";
-        private const string JWKSURL = "https://firebaseappcheck.googleapis.com/v1/jwks";
-        private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
-            + "google.identity.identitytoolkit.v1.IdentityToolkit";
-
-        private const long ClockSkewSeconds = 5 * 60;
-
-        // See http://oid-info.com/get/2.16.840.1.101.3.4.2.1
-        private const string Sha256Oid = "2.16.840.1.101.3.4.2.1";
-
-        private static readonly IReadOnlyList<string> StandardClaims =
-            ImmutableList.Create<string>("iss", "aud", "exp", "iat", "sub", "uid");
-
-        private readonly string shortName;
-        private readonly string articledShortName;
-        private readonly string operation;
-        private readonly string url;
-        private readonly string issuer;
-        private readonly IClock clock;
-        private readonly IPublicKeySource keySource;
-        private readonly AuthErrorCode invalidTokenCode;
-        private readonly AuthErrorCode expiredIdTokenCode;
-
-        internal AppCheckTokenVerify(FirebaseTokenVerifierArgs args)
-        {
-            this.ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId));
-            this.shortName = args.ShortName.ThrowIfNullOrEmpty(nameof(args.ShortName));
-            this.operation = args.Operation.ThrowIfNullOrEmpty(nameof(args.Operation));
-            this.url = args.Url.ThrowIfNullOrEmpty(nameof(args.Url));
-            this.issuer = args.Issuer.ThrowIfNullOrEmpty(nameof(args.Issuer));
-            this.clock = args.Clock ?? SystemClock.Default;
-            this.keySource = args.PublicKeySource.ThrowIfNull(nameof(args.PublicKeySource));
-            this.invalidTokenCode = args.InvalidTokenCode;
-            this.expiredIdTokenCode = args.ExpiredTokenCode;
-            this.IsEmulatorMode = args.IsEmulatorMode;
-            if ("aeiou".Contains(this.shortName.ToLower().Substring(0, 1)))
-            {
-                this.articledShortName = $"an {this.shortName}";
-            }
-            else
-            {
-                this.articledShortName = $"a {this.shortName}";
-            }
-        }
-
-        internal string ProjectId { get; }
-
-        internal bool IsEmulatorMode { get; }
-
-        internal static AppCheckTokenVerify Create(FirebaseApp app)
-        {
-            if (app == null)
-            {
-                throw new ArgumentNullException(nameof(app));
-            }
-
-            var projectId = app.GetProjectId();
-            if (string.IsNullOrEmpty(projectId))
-            {
-                throw new ArgumentException(
-                    "Must initialize FirebaseApp with a project ID to verify session cookies.");
-            }
-
-            var keySource = new HttpPublicKeySource(
-                JWKSURL, SystemClock.Default, app.Options.HttpClientFactory);
-            return Create(projectId, keySource);
-        }
-
-        internal static AppCheckTokenVerify Create(
-            string projectId,
-            IPublicKeySource keySource,
-            IClock clock = null)
-        {
-            var args = new FirebaseTokenVerifierArgs()
-            {
-                ProjectId = projectId,
-                ShortName = "session cookie",
-                Operation = "VerifySessionCookieAsync()",
-                Url = "https://firebase.google.com/docs/auth/admin/manage-cookies",
-                Issuer = "https://session.firebase.google.com/",
-                Clock = clock,
-                PublicKeySource = keySource,
-                InvalidTokenCode = AuthErrorCode.InvalidSessionCookie,
-                ExpiredTokenCode = AuthErrorCode.ExpiredSessionCookie,
-            };
-            return new AppCheckTokenVerify(args);
-        }
-
-        internal async Task<FirebaseToken> VerifyTokenAsync(
-            string token, CancellationToken cancellationToken = default(CancellationToken))
-        {
-            if (string.IsNullOrEmpty(token))
-            {
-                throw new ArgumentException($"{this.shortName} must not be null or empty.");
-            }
-
-            string[] segments = token.Split('.');
-            if (segments.Length != 3)
-            {
-                throw this.CreateException($"Incorrect number of segments in {this.shortName}.");
-            }
-
-            var header = JwtUtils.Decode<JsonWebSignature.Header>(segments[0]);
-            var payload = JwtUtils.Decode<FirebaseToken.Args>(segments[1]);
-            var projectIdMessage = $"Make sure the {this.shortName} comes from the same Firebase "
-                + "project as the credential used to initialize this SDK.";
-            var verifyTokenMessage = $"See {this.url} for details on how to retrieve a value "
-                + $"{this.shortName}.";
-            var issuer = this.issuer + this.ProjectId;
-            string error = null;
-            var errorCode = this.invalidTokenCode;
-            var currentTimeInSeconds = this.clock.UnixTimestamp();
-
-            if (!this.IsEmulatorMode && string.IsNullOrEmpty(header.KeyId))
-            {
-                if (payload.Audience == FirebaseAudience)
-                {
-                    error = $"{this.operation} expects {this.articledShortName}, but was given a custom "
-                        + "token.";
-                }
-                else if (header.Algorithm == "HS256")
-                {
-                    error = $"{this.operation} expects {this.articledShortName}, but was given a legacy "
-                        + "custom token.";
-                }
-                else
-                {
-                    error = $"Firebase {this.shortName} has no 'kid' claim.";
-                }
-            }
-            else if (!this.IsEmulatorMode && header.Algorithm != "RS256")
-            {
-                error = $"Firebase {this.shortName} has incorrect algorithm. Expected RS256 but got "
-                    + $"{header.Algorithm}. {verifyTokenMessage}";
-            }
-            else if (this.ProjectId != payload.Audience)
-            {
-                error = $"Firebase {this.shortName} has incorrect audience (aud) claim. Expected "
-                    + $"{this.ProjectId} but got {payload.Audience}. {projectIdMessage} "
-                    + $"{verifyTokenMessage}";
-            }
-            else if (payload.Issuer != issuer)
-            {
-                error = $"Firebase {this.shortName} has incorrect issuer (iss) claim. Expected "
-                    + $"{issuer} but got {payload.Issuer}.  {projectIdMessage} {verifyTokenMessage}";
-            }
-            else if (payload.IssuedAtTimeSeconds - ClockSkewSeconds > currentTimeInSeconds)
-            {
-                error = $"Firebase {this.shortName} issued at future timestamp "
-                    + $"{payload.IssuedAtTimeSeconds}. Expected to be less than "
-                    + $"{currentTimeInSeconds}.";
-            }
-            else if (payload.ExpirationTimeSeconds + ClockSkewSeconds < currentTimeInSeconds)
-            {
-                error = $"Firebase {this.shortName} expired at {payload.ExpirationTimeSeconds}. "
-                    + $"Expected to be greater than {currentTimeInSeconds}.";
-                errorCode = this.expiredIdTokenCode;
-            }
-            else if (string.IsNullOrEmpty(payload.Subject))
-            {
-                error = $"Firebase {this.shortName} has no or empty subject (sub) claim.";
-            }
-            else if (payload.Subject.Length > 128)
-            {
-                error = $"Firebase {this.shortName} has a subject claim longer than 128 characters.";
-            }
-
-            if (error != null)
-            {
-                throw this.CreateException(error, errorCode);
-            }
-
-            await this.VerifySignatureAsync(segments, header.KeyId, cancellationToken)
-                .ConfigureAwait(false);
-            var allClaims = JwtUtils.Decode<Dictionary<string, object>>(segments[1]);
-
-            // Remove standard claims, so that only custom claims would remain.
-            foreach (var claim in StandardClaims)
-            {
-                allClaims.Remove(claim);
-            }
-
-            payload.Claims = allClaims.ToImmutableDictionary();
-            return new FirebaseToken(payload);
-        }
-
-        /// <summary>
-        /// Verifies the integrity of a JWT by validating its signature. The JWT must be specified
-        /// as an array of three segments (header, body and signature).
-        /// </summary>
-        [SuppressMessage(
-            "StyleCop.Analyzers",
-            "SA1009:ClosingParenthesisMustBeSpacedCorrectly",
-            Justification = "Use of directives.")]
-        [SuppressMessage(
-            "StyleCop.Analyzers",
-            "SA1111:ClosingParenthesisMustBeOnLineOfLastParameter",
-            Justification = "Use of directives.")]
-        private async Task VerifySignatureAsync(
-            string[] segments, string keyId, CancellationToken cancellationToken)
-        {
-            if (this.IsEmulatorMode)
-            {
-                cancellationToken.ThrowIfCancellationRequested();
-                return;
-            }
-
-            byte[] hash;
-            using (var hashAlg = SHA256.Create())
-            {
-                hash = hashAlg.ComputeHash(
-                    Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
-            }
-
-            var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
-            var keys = await this.keySource.GetPublicKeysAsync(cancellationToken)
-                .ConfigureAwait(false);
-            var verified = keys.Any(key =>
-                key.Id == keyId && key.RSA.VerifyHash(
-                    hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
-            );
-            if (!verified)
-            {
-                throw this.CreateException($"Failed to verify {this.shortName} signature.");
-            }
-        }
-
-        private FirebaseAuthException CreateException(
-            string message, AuthErrorCode? errorCode = null)
-        {
-            if (errorCode == null)
-            {
-                errorCode = this.invalidTokenCode;
-            }
-
-            return new FirebaseAuthException(ErrorCode.InvalidArgument, message, errorCode);
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs
deleted file mode 100644
index 8cc05641..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyResponse.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using FirebaseAdmin.Auth;
-
-namespace FirebaseAdmin.Check
-{
-    /// <summary>
-    /// AppCheckVerifyResponse.
-    /// </summary>
-    public class AppCheckVerifyResponse(string appId, string verifiedToken, bool alreadyConsumed = false)
-    {
-        /// <summary>
-        /// Gets or sets a value indicating whether gets the Firebase App Check token.
-        /// </summary>
-        public bool AlreadyConsumed { get; set; } = alreadyConsumed;
-
-        /// <summary>
-        /// Gets or sets the Firebase App Check token.
-        /// </summary>
-        public string AppId { get; set; } = appId;
-
-        /// <summary>
-        /// Gets or sets the Firebase App Check VerifiedToken.
-        /// </summary>
-        public string VerifiedToken { get; set; } = verifiedToken;
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenOptions.cs
new file mode 100644
index 00000000..5daafc96
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenOptions.cs
@@ -0,0 +1,26 @@
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Class representing options for the AppCheck.VerifyToken method.
+    /// </summary>
+    public class AppCheckVerifyTokenOptions
+    {
+        /// <summary>
+        /// Gets or sets a value indicating whether to use the replay protection feature, set this to true. The AppCheck.VerifyToken
+        /// method will mark the token as consumed after verifying it.
+        ///
+        /// Tokens that are found to be already consumed will be marked as such in the response.
+        ///
+        /// Tokens are only considered to be consumed if it is sent to App Check backend by calling the
+        /// AppCheck.VerifyToken method with this field set to true; other uses of the token
+        /// do not consume it.
+        ///
+        /// This replay protection feature requires an additional network call to the App Check backend
+        /// and forces your clients to obtain a fresh attestation from your chosen attestation providers.
+        /// This can therefore negatively impact performance and can potentially deplete your attestation
+        /// providers' quotas faster. We recommend that you use this feature only for protecting
+        /// low volume, security critical, or expensive operations.
+        /// </summary>
+        public bool Consume { get; set; } = false;
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenResponse.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenResponse.cs
new file mode 100644
index 00000000..397caba5
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckVerifyTokenResponse.cs
@@ -0,0 +1,23 @@
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Interface representing a verified App Check token response.
+    /// </summary>
+    public class AppCheckVerifyTokenResponse
+    {
+        /// <summary>
+        /// Gets or sets App ID corresponding to the App the App Check token belonged to.
+        /// </summary>
+        public string AppId { get; set; }
+
+        /// <summary>
+        /// Gets or sets decoded Firebase App Check token.
+        /// </summary>
+        public AppCheckDecodedToken Token { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether already conumed.
+        /// </summary>
+        public bool AlreadyConsumed { get; set; }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheck.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheck.cs
new file mode 100644
index 00000000..1a61b1f9
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheck.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Asynchronously creates a new Firebase App Check token for the specified Firebase app.
+    /// </summary>
+    /// <returns>A task that completes with the creation of a new App Check token.</returns>
+    /// <exception cref="ArgumentNullException">Thrown if an error occurs while creating the custom token.</exception>
+    /// <value>The Firebase app instance.</value>
+    public sealed class FirebaseAppCheck : IFirebaseService
+    {
+        private readonly AppCheckApiClient appCheckApiClient;
+        private readonly AppCheckTokenFactory appCheckTokenFactory;
+        private readonly AppCheckTokenVerifier appCheckTokenVerifier;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="FirebaseAppCheck"/> class.
+        /// </summary>
+        /// <param name="app"> Initailize FirebaseApp. </param>
+        public FirebaseAppCheck(FirebaseApp app)
+        {
+            this.appCheckApiClient = AppCheckApiClient.Create(app);
+            this.appCheckTokenFactory = AppCheckTokenFactory.Create(app);
+            this.appCheckTokenVerifier = AppCheckTokenVerifier.Create(app);
+        }
+
+        /// <summary>
+        /// Gets the messaging instance associated with the default Firebase app. This property is
+        /// <c>null</c> if the default app doesn't yet exist.
+        /// </summary>
+        public static FirebaseAppCheck DefaultInstance
+        {
+            get
+            {
+                var app = FirebaseApp.DefaultInstance;
+                if (app == null)
+                {
+                    return null;
+                }
+
+                return GetAppCheck(app);
+            }
+        }
+
+        /// <summary>
+        /// Returns the messaging instance for the specified app.
+        /// </summary>
+        /// <returns>The <see cref="FirebaseAppCheck"/> instance associated with the specified
+        /// app.</returns>
+        /// <exception cref="System.ArgumentNullException">If the app argument is null.</exception>
+        /// <param name="app">An app instance.</param>
+        public static FirebaseAppCheck GetAppCheck(FirebaseApp app)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException("App argument must not be null.");
+            }
+
+            return app.GetOrInit<FirebaseAppCheck>(typeof(FirebaseAppCheck).Name, () =>
+            {
+                return new FirebaseAppCheck(app);
+            });
+        }
+
+        /// <summary>
+        /// Creates a new AppCheckToken that can be sent back to a client.
+        /// </summary>
+        /// <param name="appId">The app ID to use as the JWT app_id.</param>
+        /// <param name="options">Optional options object when creating a new App Check Token.</param>
+        /// <returns>A <see cref="Task{AppCheckToken}"/> A promise that fulfills with a `AppCheckToken`.</returns>
+        public async Task<AppCheckToken> CreateTokenAsync(string appId, AppCheckTokenOptions options = null)
+        {
+            string customToken = await this.appCheckTokenFactory.CreateCustomTokenAsync(appId, options, default(CancellationToken)).ConfigureAwait(false);
+
+            return await this.appCheckApiClient.ExchangeTokenAsync(customToken, appId).ConfigureAwait(false);
+        }
+
+        /// <summary>
+        /// Verifies a Firebase App Check token (JWT). If the token is valid, the promise is
+        /// fulfilled with the token's decoded claims; otherwise, the promise is
+        /// rejected.
+        /// </summary>
+        /// <param name="appCheckToken"> TThe App Check token to verify.</param>
+        /// <param name="options">  Optional {@link VerifyAppCheckTokenOptions}  object when verifying an App Check Token.</param>
+        /// <returns>A <see cref="Task{VerifyAppCheckTokenResponse}"/> A promise fulfilled with the token's decoded claims if the App Check token is valid; otherwise, a rejected promise.</returns>
+        public async Task<AppCheckVerifyTokenResponse> VerifyTokenAsync(string appCheckToken, AppCheckVerifyTokenOptions options = null)
+        {
+            if (string.IsNullOrEmpty(appCheckToken))
+            {
+                throw new FirebaseAppCheckException(
+                    ErrorCode.InvalidArgument,
+                    $"App check token {appCheckToken} must be a non - empty string.",
+                    AppCheckErrorCode.InvalidArgument);
+            }
+
+            AppCheckDecodedToken decodedToken = await this.appCheckTokenVerifier.VerifyTokenAsync(appCheckToken).ConfigureAwait(false);
+
+            if (options.Consume)
+            {
+                bool alreadyConsumed = await this.appCheckApiClient.VerifyReplayProtectionAsync(appCheckToken).ConfigureAwait(false);
+
+                return new AppCheckVerifyTokenResponse()
+                {
+                    AppId = decodedToken.AppId,
+                    AlreadyConsumed = alreadyConsumed,
+                    Token = decodedToken,
+                };
+            }
+
+            return new AppCheckVerifyTokenResponse()
+            {
+                AppId = decodedToken.AppId,
+                Token = decodedToken,
+            };
+        }
+
+        /// <summary>
+        /// Deletes this <see cref="AppCheckApiClient"/> service instance.
+        /// </summary>
+        void IFirebaseService.Delete()
+        {
+            this.appCheckApiClient.Dispose();
+            this.appCheckTokenFactory.Dispose();
+        }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheckException.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheckException.cs
new file mode 100644
index 00000000..55127d66
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/FirebaseAppCheckException.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Net.Http;
+
+namespace FirebaseAdmin.AppCheck
+{
+    /// <summary>
+    /// Exception type raised by Firebase AppCheck APIs.
+    /// </summary>
+    public sealed class FirebaseAppCheckException : FirebaseException
+    {
+        internal FirebaseAppCheckException(
+            ErrorCode code,
+            string message,
+            AppCheckErrorCode? fcmCode = null,
+            Exception inner = null,
+            HttpResponseMessage response = null)
+        : base(code, message, inner, response)
+        {
+            this.AppCheckErrorCode = fcmCode;
+        }
+
+        /// <summary>
+        /// Gets the Firease AppCheck error code associated with this exception. May be null.
+        /// </summary>
+        public AppCheckErrorCode? AppCheckErrorCode { get; private set; }
+    }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs
deleted file mode 100644
index 0ea2eb8a..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckApiClient.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace FirebaseAdmin.Check
-{
-    /// <summary>
-    /// Interface of Firebase App Check backend API .
-    /// </summary>
-    public interface IAppCheckApiClient
-    {
-        /// <summary>
-        /// Exchange a signed custom token to App Check token.
-        /// </summary>
-        /// <param name="customToken">The custom token to be exchanged.</param>
-        /// <param name="appId">The mobile App ID.</param>
-        /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
-        public Task<AppCheckToken> ExchangeTokenAsync(string customToken, string appId);
-
-        /// <summary>
-        /// Exchange a signed custom token to App Check token.
-        /// </summary>
-        /// <param name="token"> The custom token to be exchanged. </param>
-        /// <returns>A alreadyConsumed is true.</returns>
-        public Task<bool> VerifyReplayProtection(string token);
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs
deleted file mode 100644
index a24554b9..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenGenerator.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using FirebaseAdmin.Auth.Jwt;
-
-namespace FirebaseAdmin.Check
-{
-    /// <summary>
-    /// App Check Token generator.
-    /// </summary>
-    internal interface IAppCheckTokenGenerator
-    {
-        /// <summary>
-        /// Verifies the integrity of a JWT by validating its signature.
-        /// </summary>
-        /// <param name="app">Appcheck Generate Token.</param>
-        /// <returns>AppCheckTokenVerify.</returns>
-        AppCheckTokenGenerator Create(FirebaseApp app);
-
-        /// <summary>
-        /// Verifies the integrity of a JWT by validating its signature.
-        /// </summary>
-        /// <param name="appId">The Id of FirebaseApp.</param>
-        /// <param name="options">AppCheck Token Option.</param>
-        /// <param name="cancellationToken">Cancelation Token.</param>
-        /// <returns>A <see cref="Task{FirebaseToken}"/> representing the result of the asynchronous operation.</returns>
-        Task<string> CreateCustomTokenAsync(
-            string appId,
-            AppCheckTokenOptions options = null,
-            CancellationToken cancellationToken = default);
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs
deleted file mode 100644
index cc01d99b..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/IAppCheckTokenVerify.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using FirebaseAdmin.Auth;
-
-namespace FirebaseAdmin.Check
-{
-    /// <summary>
-    /// App Check Verify.
-    /// </summary>
-    internal interface IAppCheckTokenVerify
-    {
-        /// <summary>
-        /// Verifies the integrity of a JWT by validating its signature.
-        /// </summary>
-        /// <param name="app">Appcheck Generate Token.</param>
-        /// <returns>AppCheckTokenVerify.</returns>
-        AppCheckTokenVerify Create(FirebaseApp app);
-
-        /// <summary>
-        /// Verifies the integrity of a JWT by validating its signature.
-        /// </summary>
-        /// <param name="token">Appcheck Generate Token.</param>
-        /// <param name="cancellationToken">cancellaton Token.</param>
-        /// <returns>A <see cref="Task{FirebaseToken}"/> representing the result of the asynchronous operation.</returns>
-        Task<FirebaseToken> VerifyTokenAsync(
-            string token, CancellationToken cancellationToken = default(CancellationToken));
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs
deleted file mode 100644
index e664acc0..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/Key.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-
-namespace FirebaseAdmin
-{ /// <summary>
-  /// Represents a cryptographic key.
-  /// </summary>
-    public class Key
-    {
-        /// <summary>
-        /// Gets or sets the key type.
-        /// </summary>
-        public string Kty { get; set; }
-
-        /// <summary>
-        /// Gets or sets the intended use of the key.
-        /// </summary>
-        public string Use { get; set; }
-
-        /// <summary>
-        /// Gets or sets the algorithm associated with the key.
-        /// </summary>
-        public string Alg { get; set; }
-
-        /// <summary>
-        /// Gets or sets the key ID.
-        /// </summary>
-        public string Kid { get; set; }
-
-        /// <summary>
-        /// Gets or sets the modulus for the RSA public key.
-        /// </summary>
-        public string N { get; set; }
-
-        /// <summary>
-        /// Gets or sets the exponent for the RSA public key.
-        /// </summary>
-        public string E { get; set; }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/KeysRoot.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/KeysRoot.cs
deleted file mode 100644
index 38913f1a..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/KeysRoot.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace FirebaseAdmin.Check
-{
-    /// <summary>
-    /// Represents a cryptographic key.
-    /// </summary>
-    public class KeysRoot
-    {
-        /// <summary>
-        /// Gets or sets represents a cryptographic key.
-        /// </summary>
-        public List<Key> Keys { get; set; }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/VerifyAppCheckTokenOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/VerifyAppCheckTokenOptions.cs
deleted file mode 100644
index 000adac5..00000000
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/VerifyAppCheckTokenOptions.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-/// <summary>
-/// Class representing options for the AppCheck.VerifyToken method.
-/// </summary>
-public class VerifyAppCheckTokenOptions
-{
-    /// <summary>
-    /// Gets or sets a value indicating whether to use the replay protection feature, set this to true. The AppCheck.VerifyToken
-    /// method will mark the token as consumed after verifying it.
-    ///
-    /// Tokens that are found to be already consumed will be marked as such in the response.
-    ///
-    /// Tokens are only considered to be consumed if it is sent to App Check backend by calling the
-    /// AppCheck.VerifyToken method with this field set to true; other uses of the token
-    /// do not consume it.
-    ///
-    /// This replay protection feature requires an additional network call to the App Check backend
-    /// and forces your clients to obtain a fresh attestation from your chosen attestation providers.
-    /// This can therefore negatively impact performance and can potentially deplete your attestation
-    /// providers' quotas faster. We recommend that you use this feature only for protecting
-    /// low volume, security critical, or expensive operations.
-    /// </summary>
-    public bool Consume { get; set; } = false;
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
index e9ae348a..fa33bf65 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
@@ -26,7 +26,6 @@ public sealed class FirebaseToken
     {
         internal FirebaseToken(Args args)
         {
-            this.AppId = args.AppId;
             this.Issuer = args.Issuer;
             this.Subject = args.Subject;
             this.Audience = args.Audience;
@@ -70,11 +69,6 @@ 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.
@@ -93,14 +87,12 @@ internal FirebaseToken(Args args)
         /// <param name="v">FirebaseToken.</param>
         public static implicit operator string(FirebaseToken v)
         {
-            throw new NotImplementedException();
+            return v.Uid;
         }
 
         internal sealed class Args
         {
-            public string AppId { get; internal set; }
-
-            [JsonProperty("app_id")]
+            [JsonProperty("iss")]
             internal string Issuer { get; set; }
 
             [JsonProperty("sub")]
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
index d3343213..45070c8b 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenFactory.cs
@@ -176,61 +176,6 @@ 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")]
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
index 1d37c094..50fad5f7 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/FirebaseTokenVerifier.cs
@@ -37,8 +37,6 @@ 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";
 
@@ -161,44 +159,6 @@ 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))
         {
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs
index e8f8a303..fce692f0 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/Jwt/HttpPublicKeySource.cs
@@ -21,7 +21,6 @@
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
-using FirebaseAdmin.Check;
 using FirebaseAdmin.Util;
 using Google.Apis.Http;
 using Google.Apis.Util;
@@ -86,10 +85,6 @@ public async Task<IReadOnlyList<PublicKey>> GetPublicKeysAsync(
                             {
                                 this.cachedKeys = this.ParseKeys(response);
                             }
-                            else
-                            {
-                                this.cachedKeys = await this.ParseAppCheckKeys().ConfigureAwait(false);
-                            }
 
                             var cacheControl = response.HttpResponse.Headers.CacheControl;
                             if (cacheControl?.MaxAge != null)
@@ -144,37 +139,6 @@ private IReadOnlyList<PublicKey> ParseKeys(DeserializedResponseInfo<Dictionary<s
             return builder.ToImmutableList();
         }
 
-        private async Task<IReadOnlyList<PublicKey>> ParseAppCheckKeys()
-        {
-            try
-            {
-                using var client = new HttpClient();
-                HttpResponseMessage response = await client.GetAsync(this.certUrl).ConfigureAwait(false);
-                if (response.StatusCode == HttpStatusCode.OK)
-                {
-                    string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-                    KeysRoot keysRoot = JsonConvert.DeserializeObject<KeysRoot>(responseString);
-                    var builder = ImmutableList.CreateBuilder<PublicKey>();
-                    foreach (Key key in keysRoot.Keys)
-                    {
-                        var x509cert = new X509Certificate2(Encoding.UTF8.GetBytes(key.N));
-                        RSAKey rsa = x509cert.GetRSAPublicKey();
-                        builder.Add(new PublicKey(key.Kid, rsa));
-                    }
-
-                    return builder.ToImmutableList();
-                }
-                else
-                {
-                    throw new ArgumentNullException("Error Http request JwksUrl");
-                }
-            }
-            catch (Exception exception)
-            {
-                throw new ArgumentNullException("Error Http request", exception);
-            }
-        }
-
         private class HttpKeySourceErrorHandler
         : HttpErrorHandler<FirebaseAuthException>,
             IHttpRequestExceptionHandler<FirebaseAuthException>,
diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
deleted file mode 100644
index b1a6d836..00000000
--- a/FirebaseAdmin/FirebaseAdmin/FirebaseAppCheck.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml.Linq;
-using FirebaseAdmin.Auth;
-using FirebaseAdmin.Auth.Jwt;
-using FirebaseAdmin.Check;
-using Google.Apis.Logging;
-
-namespace FirebaseAdmin
-{
-    /// <summary>
-    /// Asynchronously creates a new Firebase App Check token for the specified Firebase app.
-    /// </summary>
-    /// <returns>A task that completes with the creation of a new App Check token.</returns>
-    /// <exception cref="ArgumentNullException">Thrown if an error occurs while creating the custom token.</exception>
-    /// <value>The Firebase app instance.</value>
-    public sealed class FirebaseAppCheck
-    {
-        private const string DefaultProjectId = "[DEFAULT]";
-        private static Dictionary<string, FirebaseAppCheck> appChecks = new Dictionary<string, FirebaseAppCheck>();
-
-        private readonly AppCheckApiClient apiClient;
-        private readonly AppCheckTokenVerify appCheckTokenVerifier;
-        private readonly AppCheckTokenGenerator tokenFactory;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="FirebaseAppCheck"/> class.
-        /// </summary>
-        /// <param name="value"> Initailize FirebaseApp. </param>
-        public FirebaseAppCheck(FirebaseApp value)
-        {
-            this.apiClient = new AppCheckApiClient(value);
-            this.tokenFactory = AppCheckTokenGenerator.Create(value);
-            this.appCheckTokenVerifier = AppCheckTokenVerify.Create(value);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="FirebaseAppCheck"/> class.
-        /// </summary>
-        /// <param name="app"> Initailize FirebaseApp. </param>
-        /// <returns>A <see cref="Task{FirebaseAppCheck}"/> Representing the result of the asynchronous operation.</returns>
-        public static FirebaseAppCheck Create(FirebaseApp app)
-        {
-            if (app == null)
-            {
-                throw new ArgumentNullException("FirebaseApp must not be null or empty");
-            }
-
-            string appId = app.Name;
-
-            lock (appChecks)
-            {
-                if (appChecks.ContainsKey(appId))
-                {
-                    if (appId == DefaultProjectId)
-                    {
-                        throw new ArgumentException("The default FirebaseAppCheck already exists.");
-                    }
-                    else
-                    {
-                        throw new ArgumentException($"FirebaseApp named {appId} already exists.");
-                    }
-                }
-            }
-
-            var appCheck = new FirebaseAppCheck(app);
-            appChecks.Add(appId, appCheck);
-            return appCheck;
-        }
-
-        /// <summary>
-        /// Creates a new AppCheckToken that can be sent back to a client.
-        /// </summary>
-        /// <param name="appId">The app ID to use as the JWT app_id.</param>
-        /// <param name="options">Optional options object when creating a new App Check Token.</param>
-        /// <returns>A <see cref="Task{AppCheckToken}"/> Representing the result of the asynchronous operation.</returns>
-        public async Task<AppCheckToken> CreateToken(string appId, AppCheckTokenOptions options = null)
-        {
-            string customToken = await this.tokenFactory.CreateCustomTokenAsync(appId, options, default(CancellationToken))
-                .ConfigureAwait(false);
-
-            return await this.apiClient.ExchangeTokenAsync(customToken, appId).ConfigureAwait(false);
-        }
-
-        /// <summary>
-        /// Verifies a Firebase App Check token (JWT). If the token is valid, the promise is
-        /// fulfilled with the token's decoded claims; otherwise, the promise is
-        /// rejected.
-        /// </summary>
-        /// <param name="appCheckToken"> TThe App Check token to verify.</param>
-        /// <param name="options"> Optional VerifyAppCheckTokenOptions object when verifying an App Check Token.</param>
-        /// <returns>A <see cref="Task{VerifyAppCheckTokenResponse}"/> representing the result of the asynchronous operation.</returns>
-        public async Task<AppCheckVerifyResponse> VerifyToken(string appCheckToken, VerifyAppCheckTokenOptions options = null)
-        {
-            if (string.IsNullOrEmpty(appCheckToken))
-            {
-                throw new ArgumentNullException("App check token " + appCheckToken + " must be a non - empty string.");
-            }
-
-            FirebaseToken verifiedToken = await this.appCheckTokenVerifier.VerifyTokenAsync(appCheckToken).ConfigureAwait(false);
-            bool alreadyConsumed = await this.apiClient.VerifyReplayProtection(verifiedToken).ConfigureAwait(false);
-            AppCheckVerifyResponse result;
-
-            if (!alreadyConsumed)
-            {
-                result = new AppCheckVerifyResponse(verifiedToken.AppId, verifiedToken, alreadyConsumed);
-            }
-            else
-            {
-                result = new AppCheckVerifyResponse(verifiedToken.AppId, verifiedToken);
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Deleted all the appChecks created so far. Used for unit testing.
-        /// </summary>
-        internal static void DeleteAll()
-        {
-            FirebaseApp.DeleteAll();
-            appChecks.Clear();
-        }
-    }
-}
diff --git a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
index 8b57c7ec..c523f533 100644
--- a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
@@ -15,6 +15,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Net.Http;
+using System.Text.RegularExpressions;
 using Google.Apis.Json;
 using Newtonsoft.Json.Linq;
 
@@ -29,6 +30,17 @@ internal sealed class HttpUtils
 
         private HttpUtils() { }
 
+        public static 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;
+        }
+
         internal static string EncodeQueryParams(IDictionary<string, object> queryParams)
         {
             var queryString = string.Empty;

From ec540fde685425dd533920373ddcf450c6421cca Mon Sep 17 00:00:00 2001
From: pavlo <taeul930217@skiff.com>
Date: Mon, 29 Jan 2024 00:55:56 -0500
Subject: [PATCH 6/6] feature/app-check/ReplacePlaceholders

---
 .../AppCheck/AppCheckApiClient.cs             | 10 ++++-----
 FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs | 11 ----------
 .../FirebaseAdmin/Util/StringUtils.cs         | 21 +++++++++++++++++++
 3 files changed, 26 insertions(+), 16 deletions(-)
 create mode 100644 FirebaseAdmin/FirebaseAdmin/Util/StringUtils.cs

diff --git a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
index 7b8c3446..cad60e61 100644
--- a/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppCheck/AppCheckApiClient.cs
@@ -23,12 +23,12 @@ internal sealed class AppCheckApiClient : IDisposable
 
         internal AppCheckApiClient(Args args)
         {
-            string noProjectId = "Project ID is required to access app check service. Use a service account "
+            if (string.IsNullOrEmpty(args.ProjectId))
+            {
+                string noProjectId = "Project ID is required to access app check service. Use a service account "
                     + "credential or set the project ID explicitly via AppOptions. Alternatively "
                     + "you can set the project ID via the GOOGLE_CLOUD_PROJECT environment "
                     + "variable.";
-            if (string.IsNullOrEmpty(args.ProjectId))
-            {
                 throw new FirebaseAppCheckException(
                     ErrorCode.InvalidArgument,
                     noProjectId,
@@ -216,7 +216,7 @@ private string GetUrl(string appId)
                 { "appId", appId },
             };
 
-            return HttpUtils.FormatString(AppCheckUrlFormat, urlParams);
+            return StringUtils.ReplacePlaceholders(AppCheckUrlFormat, urlParams);
         }
 
         private int StringToMilliseconds(string duration)
@@ -244,7 +244,7 @@ private string GetVerifyTokenUrl()
                 { "projectId", this.projectId },
             };
 
-            return HttpUtils.FormatString(OneTimeUseTokenVerificationUrlFormat, urlParams);
+            return StringUtils.ReplacePlaceholders(OneTimeUseTokenVerificationUrlFormat, urlParams);
         }
 
         internal sealed class Args
diff --git a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
index c523f533..55419bdc 100644
--- a/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
@@ -30,17 +30,6 @@ internal sealed class HttpUtils
 
         private HttpUtils() { }
 
-        public static 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;
-        }
-
         internal static string EncodeQueryParams(IDictionary<string, object> queryParams)
         {
             var queryString = string.Empty;
diff --git a/FirebaseAdmin/FirebaseAdmin/Util/StringUtils.cs b/FirebaseAdmin/FirebaseAdmin/Util/StringUtils.cs
new file mode 100644
index 00000000..fc626ee7
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Util/StringUtils.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace FirebaseAdmin.Util
+{
+    internal class StringUtils
+    {
+        public static string ReplacePlaceholders(string str, Dictionary<string, string> urlParams)
+        {
+            string formatted = str;
+            foreach (var key in urlParams.Keys)
+            {
+                formatted = Regex.Replace(formatted, $"{{{key}}}", urlParams[key]);
+            }
+
+            return formatted;
+        }
+    }
+}