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
{
///
/// Class that facilitates sending requests to the Firebase App Check backend API.
///
- /// A task that completes with the creation of a new App Check token.
- /// Thrown if an error occurs while creating the custom token.
- /// The Firebase app instance.
- 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;
- ///
- /// Initializes a new instance of the class.
- ///
- /// Initailize FirebaseApp.
- public AppCheckApiClient(FirebaseApp value)
+ private readonly ErrorHandlingHttpClient 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(
+ new ErrorHandlingHttpClientArgs()
+ {
+ 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();
}
///
/// Exchange a signed custom token to App Check token.
///
- /// The custom token to be exchanged.
- /// The mobile App ID.
- /// A A promise that fulfills with a `AppCheckToken`.
+ /// The custom token to be exchanged.
+ /// The mobile App ID.
+ /// A promise that fulfills with a `AppCheckToken`.
public async Task 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(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;
}
- ///
- /// Exchange a signed custom token to App Check token.
- ///
- /// The custom token to be exchanged.
- /// A alreadyConsumed is true.
- public async Task VerifyReplayProtection(string token)
+ public async Task 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(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;
}
- ///
- /// Get Verify Token Url .
- ///
- /// A formatted verify token url.
- private string GetVerifyTokenUrl()
+ internal static AppCheckApiClient Create(FirebaseApp app)
{
- var urlParams = new Dictionary
+ 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);
}
- ///
- /// Get url from FirebaseApp Id .
- ///
- /// The FirebaseApp Id.
- /// A formatted verify token url.
- 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
@@ -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);
}
- ///
- /// Converts a duration string with the suffix `s` to milliseconds.
- ///
- /// The duration as a string with the suffix "s" preceded by the number of seconds.
- /// The duration in milliseconds.
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;
}
- ///
- /// Formats a string of form 'project/{projectId}/{api}' and replaces with corresponding arguments {projectId: '1234', api: 'resource'}.
- ///
- /// The original string where the param need to be replaced.
- /// The optional parameters to replace in thestring.
- /// The resulting formatted string.
- private string FormatString(string str, Dictionary 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
+ {
+ { "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
+{
+ ///
+ /// Interface representing a decoded Firebase App Check token, returned from the {@link AppCheck.verifyToken} method..
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets or sets the issuer identifier for the issuer of the response.
+ ///
+ public string Issuer { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public string Subject { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public string[] Audience { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public int ExpirationTimeSeconds { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public int IssuedAtTimeSeconds { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public string AppId { get; set; }
+
+ ///
+ /// Gets or sets key .
+ ///
+ public Dictionary 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 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
+{
+ ///
+ /// Error codes that can be raised by the Firebase App Check APIs.
+ ///
+ public enum AppCheckErrorCode
+ {
+ ///
+ /// Process is aborted
+ ///
+ Aborted,
+
+ ///
+ /// Argument is not valid
+ ///
+ InvalidArgument,
+
+ ///
+ /// Credential is not valid
+ ///
+ InvalidCredential,
+
+ ///
+ /// The server internal error
+ ///
+ InternalError,
+
+ ///
+ /// Permission is denied
+ ///
+ PermissionDenied,
+
+ ///
+ /// Unauthenticated
+ ///
+ Unauthenticated,
+
+ ///
+ /// Resource is not found
+ ///
+ NotFound,
+
+ ///
+ /// App Check Token is expired
+ ///
+ AppCheckTokenExpired,
+
+ ///
+ /// Unknown Error
+ ///
+ 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
+{
+ ///
+ /// Parses error responses received from the Auth service, and creates instances of
+ /// .
+ ///
+ internal sealed class AppCheckErrorHandler
+ : HttpErrorHandler,
+ IHttpRequestExceptionHandler,
+ IDeserializeExceptionHandler
+ {
+ internal static readonly AppCheckErrorHandler Instance = new AppCheckErrorHandler();
+
+ private static readonly IReadOnlyDictionary CodeToErrorInfo =
+ new Dictionary()
+ {
+ {
+ "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(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();
+ }
+ }
+
+ ///
+ /// Describes a class of errors that can be raised by the Firebase Auth backend API.
+ ///
+ 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; }
+
+ ///
+ /// Gets the Firebase Auth error code extracted from the response. Returns empty string
+ /// if the error code cannot be determined.
+ ///
+ internal string Code
+ {
+ get
+ {
+ var separator = this.GetSeparator();
+ if (separator != -1)
+ {
+ return this.Message.Substring(0, separator);
+ }
+
+ return this.Message ?? string.Empty;
+ }
+ }
+
+ ///
+ /// Gets the error detail sent by the Firebase Auth API. May be null.
+ ///
+ 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
{
///
/// Interface representing an App Check token.
///
- ///
- /// Initializes a new instance of the class.
- ///
/// Generator from custom token.
/// TTl value .
public class AppCheckToken(string tokenValue, int ttlValue)
{
///
- /// Gets the Firebase App Check token.
+ /// Gets or sets the Firebase App Check token.
///
- public string Token { get; } = tokenValue;
+ public string Token { get; set; } = tokenValue;
///
/// 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
{
///
/// A helper class that creates Firebase custom tokens.
///
- 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 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 CreateCustomTokenAsync(
+ ///
+ /// Creates a new custom token that can be exchanged to an App Check token.
+ ///
+ /// The mobile App ID.
+ /// Options for AppCheckToken with ttl. Possibly null.
+ /// A cancellation token to monitor the asynchronous.
+ /// A Promise fulfilled with a custom token signed with a service account key that can be exchanged to an App Check token.
+ public async Task 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 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
{
///
/// 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 StandardClaims =
+ ImmutableList.Create("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; }
+
+ ///
+ /// Verifies the format and signature of a Firebase App Check token.
+ ///
+ /// The Firebase Auth JWT token to verify.
+ /// A cancellation token to monitor the asynchronous operation.
+ /// A task that completes with a representing
+ /// a user with the specified user ID.
+ public async Task 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(segments[0]);
+ var payload = JwtUtils.Decode(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>(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);
+ }
+
+ ///
+ /// 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).
+ ///
+ 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 StandardClaims =
- ImmutableList.Create("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 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(segments[0]);
- var payload = JwtUtils.Decode(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>(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);
- }
-
- ///
- /// 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).
- ///
- [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
-{
- ///
- /// AppCheckVerifyResponse.
- ///
- public class AppCheckVerifyResponse(string appId, string verifiedToken, bool alreadyConsumed = false)
- {
- ///
- /// Gets or sets a value indicating whether gets the Firebase App Check token.
- ///
- public bool AlreadyConsumed { get; set; } = alreadyConsumed;
-
- ///
- /// Gets or sets the Firebase App Check token.
- ///
- public string AppId { get; set; } = appId;
-
- ///
- /// Gets or sets the Firebase App Check VerifiedToken.
- ///
- 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
+{
+ ///
+ /// Class representing options for the AppCheck.VerifyToken method.
+ ///
+ public class AppCheckVerifyTokenOptions
+ {
+ ///
+ /// 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.
+ ///
+ 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
+{
+ ///
+ /// Interface representing a verified App Check token response.
+ ///
+ public class AppCheckVerifyTokenResponse
+ {
+ ///
+ /// Gets or sets App ID corresponding to the App the App Check token belonged to.
+ ///
+ public string AppId { get; set; }
+
+ ///
+ /// Gets or sets decoded Firebase App Check token.
+ ///
+ public AppCheckDecodedToken Token { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether already conumed.
+ ///
+ 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
+{
+ ///
+ /// Asynchronously creates a new Firebase App Check token for the specified Firebase app.
+ ///
+ /// A task that completes with the creation of a new App Check token.
+ /// Thrown if an error occurs while creating the custom token.
+ /// The Firebase app instance.
+ public sealed class FirebaseAppCheck : IFirebaseService
+ {
+ private readonly AppCheckApiClient appCheckApiClient;
+ private readonly AppCheckTokenFactory appCheckTokenFactory;
+ private readonly AppCheckTokenVerifier appCheckTokenVerifier;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Initailize FirebaseApp.
+ public FirebaseAppCheck(FirebaseApp app)
+ {
+ this.appCheckApiClient = AppCheckApiClient.Create(app);
+ this.appCheckTokenFactory = AppCheckTokenFactory.Create(app);
+ this.appCheckTokenVerifier = AppCheckTokenVerifier.Create(app);
+ }
+
+ ///
+ /// Gets the messaging instance associated with the default Firebase app. This property is
+ /// null if the default app doesn't yet exist.
+ ///
+ public static FirebaseAppCheck DefaultInstance
+ {
+ get
+ {
+ var app = FirebaseApp.DefaultInstance;
+ if (app == null)
+ {
+ return null;
+ }
+
+ return GetAppCheck(app);
+ }
+ }
+
+ ///
+ /// Returns the messaging instance for the specified app.
+ ///
+ /// The instance associated with the specified
+ /// app.
+ /// If the app argument is null.
+ /// An app instance.
+ public static FirebaseAppCheck GetAppCheck(FirebaseApp app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException("App argument must not be null.");
+ }
+
+ return app.GetOrInit(typeof(FirebaseAppCheck).Name, () =>
+ {
+ return new FirebaseAppCheck(app);
+ });
+ }
+
+ ///
+ /// Creates a new AppCheckToken that can be sent back to a client.
+ ///
+ /// The app ID to use as the JWT app_id.
+ /// Optional options object when creating a new App Check Token.
+ /// A A promise that fulfills with a `AppCheckToken`.
+ public async Task 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// TThe App Check token to verify.
+ /// Optional {@link VerifyAppCheckTokenOptions} object when verifying an App Check Token.
+ /// A A promise fulfilled with the token's decoded claims if the App Check token is valid; otherwise, a rejected promise.
+ public async Task 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,
+ };
+ }
+
+ ///
+ /// Deletes this service instance.
+ ///
+ 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
+{
+ ///
+ /// Exception type raised by Firebase AppCheck APIs.
+ ///
+ 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;
+ }
+
+ ///
+ /// Gets the Firease AppCheck error code associated with this exception. May be null.
+ ///
+ 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
-{
- ///
- /// Interface of Firebase App Check backend API .
- ///
- public interface IAppCheckApiClient
- {
- ///
- /// Exchange a signed custom token to App Check token.
- ///
- /// The custom token to be exchanged.
- /// The mobile App ID.
- /// A representing the result of the asynchronous operation.
- public Task ExchangeTokenAsync(string customToken, string appId);
-
- ///
- /// Exchange a signed custom token to App Check token.
- ///
- /// The custom token to be exchanged.
- /// A alreadyConsumed is true.
- public Task 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
-{
- ///
- /// App Check Token generator.
- ///
- internal interface IAppCheckTokenGenerator
- {
- ///
- /// Verifies the integrity of a JWT by validating its signature.
- ///
- /// Appcheck Generate Token.
- /// AppCheckTokenVerify.
- AppCheckTokenGenerator Create(FirebaseApp app);
-
- ///
- /// Verifies the integrity of a JWT by validating its signature.
- ///
- /// The Id of FirebaseApp.
- /// AppCheck Token Option.
- /// Cancelation Token.
- /// A representing the result of the asynchronous operation.
- Task 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
-{
- ///
- /// App Check Verify.
- ///
- internal interface IAppCheckTokenVerify
- {
- ///
- /// Verifies the integrity of a JWT by validating its signature.
- ///
- /// Appcheck Generate Token.
- /// AppCheckTokenVerify.
- AppCheckTokenVerify Create(FirebaseApp app);
-
- ///
- /// Verifies the integrity of a JWT by validating its signature.
- ///
- /// Appcheck Generate Token.
- /// cancellaton Token.
- /// A representing the result of the asynchronous operation.
- Task 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
-{ ///
- /// Represents a cryptographic key.
- ///
- public class Key
- {
- ///
- /// Gets or sets the key type.
- ///
- public string Kty { get; set; }
-
- ///
- /// Gets or sets the intended use of the key.
- ///
- public string Use { get; set; }
-
- ///
- /// Gets or sets the algorithm associated with the key.
- ///
- public string Alg { get; set; }
-
- ///
- /// Gets or sets the key ID.
- ///
- public string Kid { get; set; }
-
- ///
- /// Gets or sets the modulus for the RSA public key.
- ///
- public string N { get; set; }
-
- ///
- /// Gets or sets the exponent for the RSA public key.
- ///
- 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
-{
- ///
- /// Represents a cryptographic key.
- ///
- public class KeysRoot
- {
- ///
- /// Gets or sets represents a cryptographic key.
- ///
- public List 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;
-
-///
-/// Class representing options for the AppCheck.VerifyToken method.
-///
-public class VerifyAppCheckTokenOptions
-{
- ///
- /// 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.
- ///
- public bool Consume { get; set; } = false;
-}