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 @@ - netcoreapp2.1 + net7.0 latest false true @@ -30,4 +30,4 @@ - + \ 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 @@ - netcoreapp2.1 + net7.0 false true ../../stylecop_test.ruleset 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(() => new AppCheckApiClient(null)); + } + + [Fact] + public async Task ExchangeTokenNoProjectId() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException(this.noProjectId)); + var result = await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId)); + Assert.Equal(this.noProjectId, result.Message); + } + + [Fact] + public async Task ExchangeTokenInvalidAppId() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException(this.noProjectId)); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, string.Empty)); + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, null)); + } + + [Fact] + public async Task ExchangeTokenInvalidCustomTokenAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException(this.noProjectId)); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(string.Empty, this.appId)); + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(null, this.appId)); + } + + [Fact] + public async Task ExchangeTokenFullErrorResponseAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException("not-found", "Requested entity not found")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId)); + } + + [Fact] + public async Task ExchangeTokenErrorCodeAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException("unknown-error", "Unknown server error: {}")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId)); + } + + [Fact] + public async Task ExchangeTokenFullNonJsonAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException("unknown-error", "Unexpected response with status: 404 and body: not json")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(this.testTokenToExchange, this.appId)); + } + + [Fact] + public async Task ExchangeTokenAppErrorAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException("network-error", "socket hang up")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.ExchangeTokenAsync(string.Empty, this.appId)); + } + + [Fact] + public async Task ExchangeTokenOnSuccessAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .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(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .Throws(new ArgumentException(this.noProjectId)); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange)); + } + + [Fact] + public async Task VerifyReplayInvaildTokenAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .Throws(new ArgumentException(this.noProjectId)); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.VerifyReplayProtection(string.Empty)); + } + + [Fact] + public async Task VerifyReplayFullErrorAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .Throws(new ArgumentException("not-found", "Requested entity not found")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange)); + } + + [Fact] + public async Task VerifyReplayErrorCodeAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .Throws(new ArgumentException("unknown-error", "Unknown server error: {}")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange)); + } + + [Fact] + public async Task VerifyReplayNonJsonAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .Throws(new ArgumentException("unknown-error", "Unexpected response with status: 404 and body: not json")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange)); + } + + [Fact] + public async Task VerifyReplayFirebaseAppErrorAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .Throws(new ArgumentException("network-error", "socket hang up")); + + await Assert.ThrowsAsync(() => appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange)); + } + + [Fact] + public async Task VerifyReplayAlreadyTrueAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .ReturnsAsync(true); + + bool res = await appCheckApiClient.Object.VerifyReplayProtection(this.testTokenToExchange).ConfigureAwait(false); + Assert.True(res); + } + + [Fact] + public async Task VerifyReplayAlreadyFlaseAsync() + { + var appCheckApiClient = new Mock(); + + appCheckApiClient.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .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 InvalidStrings = new List + { + 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(() => tokenGenerator.CreateCustomTokenAsync(string.Empty)); + await Assert.ThrowsAsync(() => 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(() => + 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 InvalidStrings = new List + { + 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(() => new AppCheckTokenVerify(args)); + } + + [Fact] + public void NullKeySource() + { + var args = FullyPopulatedArgs(); + args.PublicKeySource = null; + + Assert.Throws(() => new AppCheckTokenVerify(args)); + } + + [Theory] + [MemberData(nameof(InvalidStrings))] + public void InvalidShortName(string shortName) + { + var args = FullyPopulatedArgs(); + args.ShortName = shortName; + + Assert.Throws(() => new AppCheckTokenVerify(args)); + } + + [Theory] + [MemberData(nameof(InvalidStrings))] + public void InvalidIssuer(string issuer) + { + var args = FullyPopulatedArgs(); + args.Issuer = issuer; + + Assert.Throws(() => new AppCheckTokenVerify(args)); + } + + [Theory] + [MemberData(nameof(InvalidStrings))] + public void InvalidOperation(string operation) + { + var args = FullyPopulatedArgs(); + args.Operation = operation; + + Assert.Throws(() => new AppCheckTokenVerify(args)); + } + + [Theory] + [MemberData(nameof(InvalidStrings))] + public void InvalidUrl(string url) + { + var args = FullyPopulatedArgs(); + args.Url = url; + + Assert.Throws(() => 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 @@  + + net7.0 + latest + false + ../../FirebaseAdmin.snk + true + true + ../../stylecop_test.ruleset + - - netcoreapp2.1 - latest - false - ../../FirebaseAdmin.snk - true - true - ../../stylecop_test.ruleset - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - - - - - - - - - PreserveNewest - - + + + + + + PreserveNewest + + 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(() => FirebaseAppCheck.Create(null)); } [Fact] - public async Task InvalidAppIdCreateToken() + public void CreateAppCheck() { - FirebaseAppCheck invalidAppIdCreate = FirebaseAppCheck.Create(this.mockCredentialApp); - - await Assert.ThrowsAsync(() => invalidAppIdCreate.CreateToken(appId: null)); - await Assert.ThrowsAsync(() => 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(); + + // Setup the mock to throw an exception when SignDataAsync is called + createTokenMock.Setup(service => service.SignDataAsync(It.IsAny(), It.IsAny())) + .Throws(new ArgumentException(expected)); + + var options = new AppOptions() + { + Credential = GoogleCredential.FromAccessToken("token"), + }; + var app = FirebaseApp.Create(options, "4321"); + + Assert.Throws(() => FirebaseAppCheck.Create(app)); + } + + [Fact] + public async Task CreateTokenApiError() + { + var createTokenMock = new Mock(); + + createTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())).Throws(new ArgumentException("INTERAL_ERROR")); + FirebaseAppCheck createTokenFromAppId = new FirebaseAppCheck(this.mockCredentialApp); - var token = await createTokenFromAppId.CreateToken(this.appId); - Assert.IsType(token.Token); - Assert.NotNull(token.Token); - Assert.IsType(token.TtlMillis); - Assert.Equal(3600000, token.TtlMillis); + + await Assert.ThrowsAsync(() => createTokenFromAppId.CreateToken(this.appId)); } [Fact] - public async Task CreateTokenFromAppIdAndTtlMillis() + public async Task CreateTokenApiErrorOptions() { + var createTokenMock = new Mock(); + + createTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())).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(token.Token); - Assert.NotNull(token.Token); - Assert.IsType(token.TtlMillis); - Assert.Equal(1800000, token.TtlMillis); + await Assert.ThrowsAsync(() => 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(); + + createCustomTokenMock.Setup(service => service.CreateCustomTokenAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .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(); + createExchangeTokenMock.Setup(service => service.ExchangeTokenAsync(It.IsAny(), It.IsAny())) + .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(); + createTokenMock.Setup(service => service.VerifyReplayProtection(It.IsAny())) + .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(() => 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(); + verifyTokenMock.Setup(service => service.VerifyTokenAsync(It.IsAny(), It.IsAny())) + .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(() => verifyTokenInvaild.VerifyToken(null)); - await Assert.ThrowsAsync(() => verifyTokenInvaild.VerifyToken(string.Empty)); + await Assert.ThrowsAsync(() => verifyTokenInvaild.VerifyToken(null)); + await Assert.ThrowsAsync(() => 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 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; - } - - /// - /// Exchange a signed custom token to App Check token. - /// - /// The custom token to be exchanged. - /// The Id of Firebase App. - /// HttpResponseMessage . - public async Task 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 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 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; } /// @@ -128,7 +114,7 @@ public async Task VerifyReplayProtection(string token) var responseData = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); bool alreadyConsumed = (bool)responseData["data"]["alreadyConsumed"]; return alreadyConsumed; - } + } /// /// 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 +{ + /// + /// A helper class that creates Firebase custom tokens. + /// + 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 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 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 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/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 /// /// AppCheckVerifyResponse. /// - public class AppCheckVerifyResponse(string appId, FirebaseToken verifiedToken, bool alreadyConsumed = false) + public class AppCheckVerifyResponse(string appId, string verifiedToken, bool alreadyConsumed = false) { /// /// 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 /// /// Interface of Firebase App Check backend API . /// - internal interface IAppCheckApiClient + public interface IAppCheckApiClient { /// /// Exchange a signed custom token to App Check token. @@ -25,13 +25,5 @@ internal interface IAppCheckApiClient /// The custom token to be exchanged. /// A alreadyConsumed is true. public Task VerifyReplayProtection(string token); - - /// - /// Exchange a signed custom token to App Check token. - /// - /// The custom token to be exchanged. - /// The Id of Firebase App. - /// HttpResponseMessage . - public Task 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 +{ + /// + /// 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 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 +{ + /// + /// 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/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 { /// 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 appChecks = new Dictionary(); private readonly AppCheckApiClient apiClient; - private readonly FirebaseTokenVerifier appCheckTokenVerifier; - private readonly FirebaseTokenFactory tokenFactory; + private readonly AppCheckTokenVerify appCheckTokenVerifier; + private readonly AppCheckTokenGenerator tokenFactory; /// /// Initializes a new instance of the 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); } /// @@ -42,13 +43,13 @@ public FirebaseAppCheck(FirebaseApp value) /// A Representing the result of the asynchronous operation. 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) /// A Representing the result of the asynchronous operation. public async Task 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);