diff --git a/.travis.yml b/.travis.yml index 06088137..cf183702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,5 @@ matrix: script: - dotnet build FirebaseAdmin/FirebaseAdmin - dotnet build FirebaseAdmin/FirebaseAdmin.Snippets + - dotnet build FirebaseAdmin/FirebaseAdmin.IntegrationTests - dotnet test FirebaseAdmin/FirebaseAdmin.Tests diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj index c428e7b4..d146599a 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj @@ -2,8 +2,9 @@ netcoreapp2.0 - false + true + ../../stylecop_test.ruleset @@ -12,6 +13,9 @@ + + all + diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs index 1c56baf5..af4d0dce 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs @@ -17,24 +17,24 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; -using Xunit; using FirebaseAdmin; using FirebaseAdmin.Auth; using Google.Apis.Auth.OAuth2; using Google.Apis.Util; +using Xunit; namespace FirebaseAdmin.IntegrationTests { public class FirebaseAuthTest { - private const string VerifyCustomTokenUrl = + private const string VerifyCustomTokenUrl = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken"; public FirebaseAuthTest() { IntegrationTestUtils.EnsureDefaultApp(); } - + [Fact] public async Task CreateCustomToken() { @@ -50,9 +50,9 @@ public async Task CreateCustomTokenWithClaims() { var developerClaims = new Dictionary() { - {"admin", true}, - {"package", "gold"}, - {"magicNumber", 42L}, + { "admin", true }, + { "package", "gold" }, + { "magicNumber", 42L }, }; var customToken = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync( "testuser", developerClaims); @@ -72,13 +72,14 @@ public async Task CreateCustomTokenWithClaims() public async Task CreateCustomTokenWithoutServiceAccount() { var googleCred = FirebaseApp.DefaultInstance.Options.Credential; - var serviceAcct = (ServiceAccountCredential) googleCred.UnderlyingCredential; - var token = await ((ITokenAccess) googleCred).GetAccessTokenForRequestAsync(); - var app = FirebaseApp.Create(new AppOptions() - { - Credential = GoogleCredential.FromAccessToken(token), - ServiceAccountId = serviceAcct.Id, - }, "IAMSignApp"); + var serviceAcct = (ServiceAccountCredential)googleCred.UnderlyingCredential; + var token = await ((ITokenAccess)googleCred).GetAccessTokenForRequestAsync(); + var app = FirebaseApp.Create( + new AppOptions() + { + Credential = GoogleCredential.FromAccessToken(token), + ServiceAccountId = serviceAcct.Id, + }, "IAMSignApp"); try { var customToken = await FirebaseAuth.GetAuth(app).CreateCustomTokenAsync( @@ -98,7 +99,7 @@ public async Task SetCustomUserClaims() { var customClaims = new Dictionary() { - {"admin", true} + { "admin", true }, }; await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("testuser", customClaims); @@ -126,12 +127,13 @@ private static async Task SignInWithCustomTokenAsync(string customToken) var rb = new Google.Apis.Requests.RequestBuilder() { Method = Google.Apis.Http.HttpConsts.Post, - BaseUri = new Uri(VerifyCustomTokenUrl), + BaseUri = new Uri(VerifyCustomTokenUrl), }; rb.AddParameter(RequestParameterType.Query, "key", IntegrationTestUtils.GetApiKey()); var request = rb.CreateRequest(); var jsonSerializer = Google.Apis.Json.NewtonsoftJsonSerializer.Instance; - var payload = jsonSerializer.Serialize(new SignInRequest{ + var payload = jsonSerializer.Serialize(new SignInRequest + { CustomToken = customToken, ReturnSecureToken = true, }); @@ -159,6 +161,6 @@ internal class SignInRequest internal class SignInResponse { [Newtonsoft.Json.JsonProperty("idToken")] - public String IdToken { get; set; } + public string IdToken { get; set; } } } diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs index 5761de85..bee0adc0 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs @@ -26,13 +26,15 @@ internal static class IntegrationTestUtils private const string ServiceAccountFile = "./resources/integration_cert.json"; private const string ApiKeyFile = "./resources/integration_apikey.txt"; - private static readonly Lazy DefaultFirebaseApp = new Lazy(() => { - var options = new AppOptions() + private static readonly Lazy DefaultFirebaseApp = new Lazy( + () => { - Credential = GoogleCredential.FromFile(ServiceAccountFile), - }; - return FirebaseApp.Create(options); - }, true); + var options = new AppOptions() + { + Credential = GoogleCredential.FromFile(ServiceAccountFile), + }; + return FirebaseApp.Create(options); + }, true); public static FirebaseApp EnsureDefaultApp() { diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj index 2a896f89..ee80a864 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj @@ -3,6 +3,8 @@ netcoreapp2.0 false + true + ../../stylecop_test.ruleset @@ -11,6 +13,9 @@ + + all + diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs index 106daa1a..32ca9be5 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs @@ -21,9 +21,9 @@ namespace FirebaseAdmin.Snippets { - class FirebaseAppSnippets + internal class FirebaseAppSnippets { - static void InitSdkWithServiceAccount() + internal static void InitSdkWithServiceAccount() { // [START initialize_sdk_with_service_account] FirebaseApp.Create(new AppOptions() @@ -33,7 +33,7 @@ static void InitSdkWithServiceAccount() // [END initialize_sdk_with_service_account] } - static void InitSdkWithApplicationDefault() + internal static void InitSdkWithApplicationDefault() { // [START initialize_sdk_with_application_default] FirebaseApp.Create(new AppOptions() @@ -43,7 +43,7 @@ static void InitSdkWithApplicationDefault() // [END initialize_sdk_with_application_default] } - static void InitSdkWithRefreshToken() + internal static void InitSdkWithRefreshToken() { // [START initialize_sdk_with_refresh_token] FirebaseApp.Create(new AppOptions() @@ -53,14 +53,14 @@ static void InitSdkWithRefreshToken() // [END initialize_sdk_with_refresh_token] } - static void InitSdkWithDefaultConfig() + internal static void InitSdkWithDefaultConfig() { // [START initialize_sdk_with_default_config] FirebaseApp.Create(); // [END initialize_sdk_with_default_config] } - static void InitDefaultApp() + internal static void InitDefaultApp() { // [START access_services_default] // Initialize the default app @@ -78,7 +78,7 @@ static void InitDefaultApp() // [END access_services_default] } - static void InitCustomApp() + internal static void InitCustomApp() { var defaultOptions = new AppOptions() { @@ -107,7 +107,7 @@ static void InitCustomApp() // [END access_services_nondefault] } - static void InitWithServiceAccountId() + internal static void InitWithServiceAccountId() { // [START initialize_sdk_with_service_account_id] FirebaseApp.Create(new AppOptions() diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs index bb7e094e..0afb71e2 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs @@ -19,9 +19,9 @@ namespace FirebaseAdmin.Snippets { - class FirebaseAuthSnippets + internal class FirebaseAuthSnippets { - static async Task CreateCustomTokenAsync() + internal static async Task CreateCustomTokenAsync() { // [START custom_token] var uid = "some-uid"; @@ -32,7 +32,7 @@ static async Task CreateCustomTokenAsync() Console.WriteLine("Created custom token: {0}", customToken); } - static async Task CreateCustomTokenWithClaimsAsync() + internal static async Task CreateCustomTokenWithClaimsAsync() { // [START custom_token_with_claims] var uid = "some-uid"; @@ -48,7 +48,7 @@ static async Task CreateCustomTokenWithClaimsAsync() Console.WriteLine("Created custom token: {0}", customToken); } - static async Task VeridyIdTokenAsync(string idToken) + internal static async Task VeridyIdTokenAsync(string idToken) { // [START verify_id_token] FirebaseToken decodedToken = await FirebaseAuth.DefaultInstance diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs index bc8a90df..491aa9af 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs @@ -20,15 +20,17 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Xunit; +using FirebaseAdmin.Auth; +using Google.Apis.Auth; using Google.Apis.Auth.OAuth2; +using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace FirebaseAdmin.Auth.Tests { - public class FirebaseAuthTest: IDisposable + public class FirebaseAuthTest : IDisposable { - private static readonly GoogleCredential mockCredential = + private static readonly GoogleCredential MockCredential = GoogleCredential.FromAccessToken("test-token"); [Fact] @@ -40,7 +42,7 @@ public void GetAuthWithoutApp() [Fact] public void GetDefaultAuth() { - var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); + var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }); FirebaseAuth auth = FirebaseAuth.DefaultInstance; Assert.Same(auth, FirebaseAuth.DefaultInstance); app.Delete(); @@ -50,7 +52,7 @@ public void GetDefaultAuth() [Fact] public void GetAuth() { - var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}, "MyApp"); + var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }, "MyApp"); FirebaseAuth auth = FirebaseAuth.GetAuth(app); Assert.Same(auth, FirebaseAuth.GetAuth(app)); app.Delete(); @@ -60,7 +62,7 @@ public void GetAuth() [Fact] public async Task UseAfterDelete() { - var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); + var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }); FirebaseAuth auth = FirebaseAuth.DefaultInstance; app.Delete(); await Assert.ThrowsAsync( @@ -73,7 +75,7 @@ await Assert.ThrowsAsync( public async Task CreateCustomToken() { var cred = GoogleCredential.FromFile("./resources/service_account.json"); - FirebaseApp.Create(new AppOptions(){Credential = cred}); + FirebaseApp.Create(new AppOptions() { Credential = cred }); var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1"); VerifyCustomToken(token, "user1", null); } @@ -82,12 +84,12 @@ public async Task CreateCustomToken() public async Task CreateCustomTokenWithClaims() { var cred = GoogleCredential.FromFile("./resources/service_account.json"); - FirebaseApp.Create(new AppOptions(){Credential = cred}); + FirebaseApp.Create(new AppOptions() { Credential = cred }); var developerClaims = new Dictionary() { - {"admin", true}, - {"package", "gold"}, - {"magicNumber", 42L}, + { "admin", true }, + { "package", "gold" }, + { "magicNumber", 42L }, }; var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync( "user2", developerClaims); @@ -98,7 +100,7 @@ public async Task CreateCustomTokenWithClaims() public async Task CreateCustomTokenCancel() { var cred = GoogleCredential.FromFile("./resources/service_account.json"); - FirebaseApp.Create(new AppOptions(){Credential = cred}); + FirebaseApp.Create(new AppOptions() { Credential = cred }); var canceller = new CancellationTokenSource(); canceller.Cancel(); await Assert.ThrowsAsync( @@ -109,7 +111,7 @@ await Assert.ThrowsAsync( [Fact] public async Task CreateCustomTokenInvalidCredential() { - FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); + FirebaseApp.Create(new AppOptions() { Credential = MockCredential }); await Assert.ThrowsAsync( async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1")); } @@ -117,7 +119,7 @@ await Assert.ThrowsAsync( [Fact] public async Task VerifyIdTokenNoProjectId() { - FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); + FirebaseApp.Create(new AppOptions() { Credential = MockCredential }); var idToken = await FirebaseTokenVerifierTest.CreateTestTokenAsync(); await Assert.ThrowsAsync( async () => await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken)); @@ -128,7 +130,7 @@ public async Task VerifyIdTokenCancel() { FirebaseApp.Create(new AppOptions() { - Credential = mockCredential, + Credential = MockCredential, ProjectId = "test-project", }); var canceller = new CancellationTokenSource(); @@ -139,12 +141,29 @@ await Assert.ThrowsAnyAsync( idToken, canceller.Token)); } + [Fact] + public async Task SetCustomUserClaimsNoProjectId() + { + FirebaseApp.Create(new AppOptions() { Credential = MockCredential }); + var customClaims = new Dictionary() + { + { "admin", true }, + }; + await Assert.ThrowsAsync( + async () => await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("user1", customClaims)); + } + + public void Dispose() + { + FirebaseApp.DeleteAll(); + } + private static void VerifyCustomToken(string token, string uid, Dictionary claims) { - String[] segments = token.Split("."); + string[] segments = token.Split("."); Assert.Equal(3, segments.Length); - var payload = JwtUtils.Decode(segments[1]); + var payload = JwtUtils.Decode(segments[1]); Assert.Equal("client@test-project.iam.gserviceaccount.com", payload.Issuer); Assert.Equal("client@test-project.iam.gserviceaccount.com", payload.Subject); Assert.Equal(uid, payload.Uid); @@ -164,28 +183,11 @@ private static void VerifyCustomToken(string token, string uid, Dictionary() - { - {"admin", true} - }; - await Assert.ThrowsAsync( - async () => await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("user1", customClaims)); - } - - public void Dispose() - { - FirebaseApp.DeleteAll(); - } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs index 6ad7d462..fa954b82 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs @@ -20,11 +20,11 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Xunit; -using FirebaseAdmin.Tests; using FirebaseAdmin.Auth; +using FirebaseAdmin.Tests; using Google.Apis.Auth; using Google.Apis.Util; +using Xunit; namespace FirebaseAdmin.Auth.Tests { @@ -56,9 +56,9 @@ public async Task CreateCustomTokenWithClaims() var factory = new FirebaseTokenFactory(new MockSigner(), clock); var developerClaims = new Dictionary() { - {"admin", true}, - {"package", "gold"}, - {"magicNumber", 42L}, + { "admin", true }, + { "package", "gold" }, + { "magicNumber", 42L }, }; var token = await factory.CreateCustomTokenAsync("user2", developerClaims); VerifyCustomToken(token, "user2", developerClaims); @@ -71,37 +71,39 @@ public async Task InvalidUid() await Assert.ThrowsAsync( async () => await factory.CreateCustomTokenAsync(null)); await Assert.ThrowsAsync( - async () => await factory.CreateCustomTokenAsync("")); + async () => await factory.CreateCustomTokenAsync(string.Empty)); await Assert.ThrowsAsync( - async () => await factory.CreateCustomTokenAsync(new String('a', 129))); + async () => await factory.CreateCustomTokenAsync(new string('a', 129))); } [Fact] public async Task ReservedClaims() { var factory = new FirebaseTokenFactory(new MockSigner(), new MockClock()); - foreach(var key in FirebaseTokenFactory.ReservedClaims) + foreach (var key in FirebaseTokenFactory.ReservedClaims) { - var developerClaims = new Dictionary(){ - {key, "value"}, + var developerClaims = new Dictionary() + { + { key, "value" }, }; await Assert.ThrowsAsync( - async () => await factory.CreateCustomTokenAsync("user", developerClaims)); - } + async () => await factory.CreateCustomTokenAsync("user", developerClaims)); + } } private static void VerifyCustomToken( string token, string uid, Dictionary claims) { - String[] segments = token.Split("."); + string[] segments = token.Split("."); Assert.Equal(3, segments.Length); + // verify header var header = JwtUtils.Decode(segments[0]); Assert.Equal("JWT", header.Type); Assert.Equal("RS256", header.Algorithm); // verify payload - var payload = JwtUtils.Decode(segments[1]); + var payload = JwtUtils.Decode(segments[1]); Assert.Equal(MockSigner.KeyIdString, payload.Issuer); Assert.Equal(MockSigner.KeyIdString, payload.Subject); Assert.Equal(uid, payload.Uid); @@ -141,6 +143,6 @@ public Task SignDataAsync(byte[] data, CancellationToken cancellationTok return Task.FromResult(Encoding.UTF8.GetBytes(Signature)); } - public void Dispose() {} + public void Dispose() { } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs index f8bfea99..306d3b5b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs @@ -20,20 +20,23 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using Xunit; -using Google.Apis.Auth.OAuth2; -using Google.Apis.Util; using FirebaseAdmin.Auth; using FirebaseAdmin.Tests; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Util; +using Xunit; namespace FirebaseAdmin.Auth.Tests { - public class FirebaseTokenVerifierTest: IDisposable + public class FirebaseTokenVerifierTest : IDisposable { private static readonly IPublicKeySource KeySource = new FileSystemPublicKeySource( "./resources/public_cert.pem"); + private static readonly IClock Clock = new MockClock(); + private static readonly ISigner Signer = CreateTestSigner(); + private static readonly FirebaseTokenVerifier TokenVerifier = new FirebaseTokenVerifier( new FirebaseTokenVerifierArgs() { @@ -46,7 +49,7 @@ public class FirebaseTokenVerifierTest: IDisposable PublicKeySource = KeySource, }); - private static readonly GoogleCredential mockCredential = + private static readonly GoogleCredential MockCredential = GoogleCredential.FromAccessToken("test-token"); [Fact] @@ -54,17 +57,18 @@ public async Task ValidToken() { var payload = new Dictionary() { - {"foo", "bar"}, + { "foo", "bar" }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); var decoded = await TokenVerifier.VerifyTokenAsync(idToken); Assert.Equal("testuser", decoded.Uid); Assert.Equal("test-project", decoded.Audience); Assert.Equal("testuser", decoded.Subject); + // The default test token created by CreateTestTokenAsync has an issue time 10 minutes // ago, and an expiry time 50 minutes in the future. - Assert.Equal(Clock.UnixTimestamp() - 60 * 10, decoded.IssuedAtTimeSeconds); - Assert.Equal(Clock.UnixTimestamp() + 60 * 50, decoded.ExpirationTimeSeconds); + Assert.Equal(Clock.UnixTimestamp() - (60 * 10), decoded.IssuedAtTimeSeconds); + Assert.Equal(Clock.UnixTimestamp() + (60 * 50), decoded.ExpirationTimeSeconds); Assert.Single(decoded.Claims); object value; Assert.True(decoded.Claims.TryGetValue("foo", out value)); @@ -77,7 +81,7 @@ public async Task InvalidArgument() await Assert.ThrowsAsync( async () => await TokenVerifier.VerifyTokenAsync(null)); await Assert.ThrowsAsync( - async () => await TokenVerifier.VerifyTokenAsync("")); + async () => await TokenVerifier.VerifyTokenAsync(string.Empty)); } [Fact] @@ -92,7 +96,7 @@ public async Task NoKid() { var header = new Dictionary() { - {"kid", ""}, + { "kid", string.Empty }, }; var idToken = await CreateTestTokenAsync(headerOverrides: header); await Assert.ThrowsAsync( @@ -104,7 +108,7 @@ public async Task IncorrectKid() { var header = new Dictionary() { - {"kid", "incorrect-key-id"}, + { "kid", "incorrect-key-id" }, }; var idToken = await CreateTestTokenAsync(headerOverrides: header); await Assert.ThrowsAsync( @@ -116,7 +120,7 @@ public async Task IncorrectAlgorithm() { var header = new Dictionary() { - {"alg", "HS256"}, + { "alg", "HS256" }, }; var idToken = await CreateTestTokenAsync(headerOverrides: header); await Assert.ThrowsAsync( @@ -128,7 +132,7 @@ public async Task Expired() { var payload = new Dictionary() { - {"exp", Clock.UnixTimestamp() - 60}, + { "exp", Clock.UnixTimestamp() - 60 }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -140,7 +144,7 @@ public async Task InvalidIssuedAt() { var payload = new Dictionary() { - {"iat", Clock.UnixTimestamp() + 60}, + { "iat", Clock.UnixTimestamp() + 60 }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -152,7 +156,7 @@ public async Task InvalidIssuer() { var payload = new Dictionary() { - {"iss", "wrong-issuer"}, + { "iss", "wrong-issuer" }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -164,7 +168,7 @@ public async Task InvalidAudience() { var payload = new Dictionary() { - {"aud", "wrong-audience"}, + { "aud", "wrong-audience" }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -176,7 +180,7 @@ public async Task EmptySubject() { var payload = new Dictionary() { - {"sub", ""}, + { "sub", string.Empty }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -188,7 +192,7 @@ public async Task LongSubject() { var payload = new Dictionary() { - {"sub", new String('a', 129)}, + { "sub", new string('a', 129) }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -200,7 +204,7 @@ public void ProjectIdFromOptions() { var app = FirebaseApp.Create(new AppOptions() { - Credential = mockCredential, + Credential = MockCredential, ProjectId = "explicit-project-id", }); var verifier = FirebaseTokenVerifier.CreateIDTokenVerifier(app); @@ -226,14 +230,14 @@ public void ProjectIdFromEnvironment() { var app = FirebaseApp.Create(new AppOptions() { - Credential = mockCredential, + Credential = MockCredential, }); var verifier = FirebaseTokenVerifier.CreateIDTokenVerifier(app); Assert.Equal("env-project-id", verifier.ProjectId); } finally { - Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", ""); + Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", string.Empty); } } @@ -253,9 +257,9 @@ internal static async Task CreateTestTokenAsync( { var header = new Dictionary() { - {"alg", "RS256"}, - {"typ", "jwt"}, - {"kid", "test-key-id"}, + { "alg", "RS256" }, + { "typ", "jwt" }, + { "kid", "test-key-id" }, }; if (headerOverrides != null) { @@ -267,11 +271,11 @@ internal static async Task CreateTestTokenAsync( var payload = new Dictionary() { - {"sub", "testuser"}, - {"iss", "https://securetoken.google.com/test-project"}, - {"aud", "test-project"}, - {"iat", Clock.UnixTimestamp() - 60 * 10}, - {"exp", Clock.UnixTimestamp() + 60 * 50}, + { "sub", "testuser" }, + { "iss", "https://securetoken.google.com/test-project" }, + { "aud", "test-project" }, + { "iat", Clock.UnixTimestamp() - (60 * 10) }, + { "exp", Clock.UnixTimestamp() + (60 * 50) }, }; if (payloadOverrides != null) { @@ -280,32 +284,33 @@ internal static async Task CreateTestTokenAsync( payload[entry.Key] = entry.Value; } } + return await JwtUtils.CreateSignedJwtAsync(header, payload, Signer); } private static ISigner CreateTestSigner() { var credential = GoogleCredential.FromFile("./resources/service_account.json"); - var serviceAccount = (ServiceAccountCredential) credential.UnderlyingCredential; + var serviceAccount = (ServiceAccountCredential)credential.UnderlyingCredential; return new ServiceAccountSigner(serviceAccount); } } internal class FileSystemPublicKeySource : IPublicKeySource { - private IReadOnlyList _rsa; + private IReadOnlyList rsa; public FileSystemPublicKeySource(string file) { var x509cert = new X509Certificate2(File.ReadAllBytes(file)); - var rsa = (RSA) x509cert.PublicKey.Key; - _rsa = ImmutableList.Create(new PublicKey("test-key-id", rsa)); + var rsa = (RSA)x509cert.PublicKey.Key; + this.rsa = ImmutableList.Create(new PublicKey("test-key-id", rsa)); } public Task> GetPublicKeysAsync( CancellationToken cancellationToken) { - return Task.FromResult(_rsa); + return Task.FromResult(this.rsa); } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs index 3b666c49..c3114fe2 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs @@ -14,25 +14,26 @@ using System; using System.Collections.Generic; -using Xunit; -using Google.Apis.Auth.OAuth2; -using FirebaseAdmin.Tests; -using System.Threading.Tasks; using System.Net; +using System.Threading.Tasks; +using FirebaseAdmin.Tests; +using Google.Apis.Auth.OAuth2; +using Xunit; namespace FirebaseAdmin.Auth.Tests { public class FirebaseUserManagerTest { - private static readonly GoogleCredential mockCredential = + private const string MockProjectId = "project1"; + + private static readonly GoogleCredential MockCredential = GoogleCredential.FromAccessToken("test-token"); - private const string mockProjectId = "project1"; [Fact] public void InvalidUidForUserRecord() { Assert.Throws(() => new UserRecord(null)); - Assert.Throws(() => new UserRecord("")); + Assert.Throws(() => new UserRecord(string.Empty)); Assert.Throws(() => new UserRecord(new string('a', 129))); } @@ -41,18 +42,20 @@ public void ReservedClaims() { foreach (var key in FirebaseTokenFactory.ReservedClaims) { - var customClaims = new Dictionary(){ - {key, "value"}, + var customClaims = new Dictionary() + { + { key, "value" }, }; - Assert.Throws(() => new UserRecord("user1") { CustomClaims = customClaims}); + Assert.Throws(() => new UserRecord("user1") { CustomClaims = customClaims }); } } [Fact] public void EmptyClaims() { - var emptyClaims = new Dictionary(){ - {"", "value"}, + var emptyClaims = new Dictionary() + { + { string.Empty, "value" }, }; Assert.Throws(() => new UserRecord("user1") { CustomClaims = emptyClaims }); } @@ -73,18 +76,19 @@ public async Task UpdateUser() { var handler = new MockMessageHandler() { - Response = new UserRecord("user1") + Response = new UserRecord("user1"), }; var factory = new MockHttpClientFactory(handler); var userManager = new FirebaseUserManager( new FirebaseUserManagerArgs { - Credential = mockCredential, - ProjectId = mockProjectId, - ClientFactory = factory + Credential = MockCredential, + ProjectId = MockProjectId, + ClientFactory = factory, }); - var customClaims = new Dictionary(){ - {"admin", true}, + var customClaims = new Dictionary() + { + { "admin", true }, }; await userManager.UpdateUserAsync(new UserRecord("user1") { CustomClaims = customClaims }); @@ -95,18 +99,19 @@ public async Task UpdateUserIncorrectResponseObject() { var handler = new MockMessageHandler() { - Response = new object() + Response = new object(), }; var factory = new MockHttpClientFactory(handler); var userManager = new FirebaseUserManager( new FirebaseUserManagerArgs { - Credential = mockCredential, - ProjectId = mockProjectId, - ClientFactory = factory + Credential = MockCredential, + ProjectId = MockProjectId, + ClientFactory = factory, }); - var customClaims = new Dictionary(){ - {"admin", true}, + var customClaims = new Dictionary() + { + { "admin", true }, }; await Assert.ThrowsAsync( @@ -118,18 +123,19 @@ public async Task UpdateUserIncorrectResponseUid() { var handler = new MockMessageHandler() { - Response = new UserRecord("testuser") + Response = new UserRecord("testuser"), }; var factory = new MockHttpClientFactory(handler); var userManager = new FirebaseUserManager( new FirebaseUserManagerArgs { - Credential = mockCredential, - ProjectId = mockProjectId, - ClientFactory = factory + Credential = MockCredential, + ProjectId = MockProjectId, + ClientFactory = factory, }); - var customClaims = new Dictionary(){ - {"admin", true}, + var customClaims = new Dictionary() + { + { "admin", true }, }; await Assert.ThrowsAsync( @@ -141,18 +147,19 @@ public async Task UpdateUserHttpError() { var handler = new MockMessageHandler() { - StatusCode = HttpStatusCode.InternalServerError + StatusCode = HttpStatusCode.InternalServerError, }; var factory = new MockHttpClientFactory(handler); var userManager = new FirebaseUserManager( new FirebaseUserManagerArgs { - Credential = mockCredential, - ProjectId = mockProjectId, - ClientFactory = factory + Credential = MockCredential, + ProjectId = MockProjectId, + ClientFactory = factory, }); - var customClaims = new Dictionary(){ - {"admin", true}, + var customClaims = new Dictionary() + { + { "admin", true }, }; await Assert.ThrowsAsync( diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs index e7563010..cf4406b4 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs @@ -18,9 +18,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using Xunit; -using FirebaseAdmin.Tests; using FirebaseAdmin.Auth; +using FirebaseAdmin.Tests; +using Xunit; namespace FirebaseAdmin.Auth.Tests { @@ -92,7 +92,7 @@ public void InvalidArguments() Assert.Throws( () => new HttpPublicKeySource(null, clock, clientFactory)); Assert.Throws( - () => new HttpPublicKeySource("", clock, clientFactory)); + () => new HttpPublicKeySource(string.Empty, clock, clientFactory)); Assert.Throws( () => new HttpPublicKeySource("https://example.com/certs", null, clientFactory)); Assert.Throws( diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs index 20815944..3bd82579 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs @@ -19,14 +19,65 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using FirebaseAdmin.Tests; using Google.Apis.Auth.OAuth2; using Google.Apis.Http; using Google.Apis.Json; using Xunit; -using FirebaseAdmin.Tests; namespace FirebaseAdmin.Auth.Tests { + public class IAMSignerTest + { + [Fact] + public async Task Signer() + { + var bytes = Encoding.UTF8.GetBytes("signature"); + var handler = new MockMessageHandler() + { + Response = "discovered-service-account", + }; + var factory = new MockHttpClientFactory(handler); + var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token")); + Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync()); + Assert.Equal(1, handler.Calls); + + // should only fetch account once + Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync()); + Assert.Equal(1, handler.Calls); + + handler.Response = new IAMSigner.SignBlobResponse() + { + Signature = Convert.ToBase64String(bytes), + }; + byte[] data = Encoding.UTF8.GetBytes("Hello world"); + byte[] signature = await signer.SignDataAsync(data); + Assert.Equal(bytes, signature); + var req = NewtonsoftJsonSerializer.Instance.Deserialize( + handler.Request); + Assert.Equal(Convert.ToBase64String(data), req.BytesToSign); + Assert.Equal(2, handler.Calls); + } + + [Fact] + public async Task AccountDiscoveryError() + { + var bytes = Encoding.UTF8.GetBytes("signature"); + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.InternalServerError, + }; + var factory = new MockHttpClientFactory(handler); + var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token")); + await Assert.ThrowsAsync( + async () => await signer.GetKeyIdAsync()); + Assert.Equal(1, handler.Calls); + await Assert.ThrowsAsync( + async () => await signer.GetKeyIdAsync()); + Assert.Equal(1, handler.Calls); + } + } + public class FixedAccountIAMSignerTest { [Fact] @@ -35,7 +86,7 @@ public async Task Signer() var bytes = Encoding.UTF8.GetBytes("signature"); var handler = new MockMessageHandler() { - Response = new SignBlobResponse() + Response = new IAMSigner.SignBlobResponse() { Signature = Convert.ToBase64String(bytes), }, @@ -47,7 +98,7 @@ public async Task Signer() byte[] data = Encoding.UTF8.GetBytes("Hello world"); byte[] signature = await signer.SignDataAsync(data); Assert.Equal(bytes, signature); - var req = NewtonsoftJsonSerializer.Instance.Deserialize( + var req = NewtonsoftJsonSerializer.Instance.Deserialize( handler.Request); Assert.Equal(Convert.ToBase64String(data), req.BytesToSign); Assert.Equal(1, handler.Calls); @@ -59,7 +110,7 @@ public async Task WelformedSignError() var handler = new MockMessageHandler() { StatusCode = HttpStatusCode.InternalServerError, - Response = @"{""error"": {""message"": ""test reason""}}" + Response = @"{""error"": {""message"": ""test reason""}}", }; var factory = new MockHttpClientFactory(handler); var signer = new FixedAccountIAMSigner( @@ -77,7 +128,7 @@ public async Task UnexpectedSignError() var handler = new MockMessageHandler() { StatusCode = HttpStatusCode.InternalServerError, - Response = "not json" + Response = "not json", }; var factory = new MockHttpClientFactory(handler); var signer = new FixedAccountIAMSigner( @@ -89,55 +140,4 @@ public async Task UnexpectedSignError() Assert.Contains("not json", ex.Message); } } - - public class IAMSignerTest - { - [Fact] - public async Task Signer() - { - var bytes = Encoding.UTF8.GetBytes("signature"); - var handler = new MockMessageHandler() - { - Response = "discovered-service-account", - }; - var factory = new MockHttpClientFactory(handler); - var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token")); - Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync()); - Assert.Equal(1, handler.Calls); - - // should only fetch account once - Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync()); - Assert.Equal(1, handler.Calls); - - handler.Response = new SignBlobResponse() - { - Signature = Convert.ToBase64String(bytes), - }; - byte[] data = Encoding.UTF8.GetBytes("Hello world"); - byte[] signature = await signer.SignDataAsync(data); - Assert.Equal(bytes, signature); - var req = NewtonsoftJsonSerializer.Instance.Deserialize( - handler.Request); - Assert.Equal(Convert.ToBase64String(data), req.BytesToSign); - Assert.Equal(2, handler.Calls); - } - - [Fact] - public async Task AccountDiscoveryError() - { - var bytes = Encoding.UTF8.GetBytes("signature"); - var handler = new MockMessageHandler() - { - StatusCode = HttpStatusCode.InternalServerError, - }; - var factory = new MockHttpClientFactory(handler); - var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token")); - await Assert.ThrowsAsync( - async () => await signer.GetKeyIdAsync()); - Assert.Equal(1, handler.Calls); - await Assert.ThrowsAsync( - async () => await signer.GetKeyIdAsync()); - Assert.Equal(1, handler.Calls); - } - } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs index 2874a79f..cf76f125 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs @@ -18,9 +18,9 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; -using Xunit; using FirebaseAdmin.Auth; using Google.Apis.Auth.OAuth2; +using Xunit; namespace FirebaseAdmin.Auth.Tests { @@ -30,13 +30,13 @@ public class ServiceAccountSignerTest public async Task Signer() { var credential = GoogleCredential.FromFile("./resources/service_account.json"); - var serviceAccount = (ServiceAccountCredential) credential.UnderlyingCredential; + var serviceAccount = (ServiceAccountCredential)credential.UnderlyingCredential; var signer = new ServiceAccountSigner(serviceAccount); - Assert.Equal("client@test-project.iam.gserviceaccount.com", - await signer.GetKeyIdAsync()); + Assert.Equal( + "client@test-project.iam.gserviceaccount.com", await signer.GetKeyIdAsync()); byte[] data = Encoding.UTF8.GetBytes("Hello world"); byte[] signature = signer.SignDataAsync(data).Result; - Assert.True(Verify(data, signature)); + Assert.True(this.Verify(data, signature)); } [Fact] @@ -48,7 +48,7 @@ public void NullCredential() private bool Verify(byte[] data, byte[] signature) { var x509cert = new X509Certificate2(File.ReadAllBytes("./resources/public_cert.pem")); - var rsa = (RSA) x509cert.PublicKey.Key; + var rsa = (RSA)x509cert.PublicKey.Key; return rsa.VerifyData( data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj index fd4c9036..8641fa7b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj @@ -5,6 +5,8 @@ false ../../FirebaseAdmin.snk true + true + ../../stylecop_test.ruleset @@ -13,6 +15,9 @@ + + all + diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs index cc525acd..5a453821 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs @@ -14,13 +14,13 @@ using System; using System.Threading.Tasks; -using Xunit; using FirebaseAdmin; using Google.Apis.Auth.OAuth2; +using Xunit; namespace FirebaseAdmin.Tests { - public class FirebaseAppTest: IDisposable + public class FirebaseAppTest : IDisposable { private static readonly AppOptions TestOptions = new AppOptions() { @@ -96,7 +96,7 @@ public void CreateAppOptions() }; var app = FirebaseApp.Create(options); Assert.Equal("[DEFAULT]", app.Name); - + var copy = app.Options; Assert.NotSame(options, copy); Assert.Same(credential, copy.Credential); @@ -114,7 +114,7 @@ public void ServiceAccountCredentialScoping() }; var app = FirebaseApp.Create(options); Assert.Equal("[DEFAULT]", app.Name); - + var copy = app.Options; Assert.NotSame(options, copy); Assert.NotSame(credential, copy.Credential); @@ -139,7 +139,7 @@ public void ApplicationDefaultCredentials() } finally { - Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", ""); + Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", string.Empty); } } @@ -169,7 +169,7 @@ public void GetProjectIdFromServiceAccount() [Fact] public void GetProjectIdFromEnvironment() { - foreach (var name in new string[]{"GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"}) + foreach (var name in new string[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" }) { Environment.SetEnvironmentVariable(name, "env-project"); try @@ -180,7 +180,7 @@ public void GetProjectIdFromEnvironment() } finally { - Environment.SetEnvironmentVariable(name, ""); + Environment.SetEnvironmentVariable(name, string.Empty); } } } @@ -196,14 +196,15 @@ public void GetOrInitService() var service1 = app.GetOrInit("MockService", factory); var service2 = app.GetOrInit("MockService", factory); Assert.Same(service1, service2); - Assert.Throws(() => { + Assert.Throws(() => + { app.GetOrInit("MockService", () => { return new OtherMockService(); }); }); - + Assert.False(service1.Deleted); app.Delete(); Assert.True(service1.Deleted); - Assert.Throws(() => + Assert.Throws(() => { app.GetOrInit("MockService", factory); }); @@ -211,22 +212,22 @@ public void GetOrInitService() public void Dispose() { - FirebaseApp.DeleteAll(); + FirebaseApp.DeleteAll(); } } - internal class MockService: IFirebaseService + internal class MockService : IFirebaseService { public bool Deleted { get; private set; } public void Delete() { - Deleted = true; + this.Deleted = true; } } - internal class OtherMockService: IFirebaseService + internal class OtherMockService : IFirebaseService { - public void Delete() {} + public void Delete() { } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs index d4fb09e6..f68f7785 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs @@ -19,36 +19,37 @@ namespace FirebaseAdmin.Tests { public class MockClock : IClock { - public DateTime Now + private object mutex = new object(); + private DateTime utcNow; + + public MockClock() { - get { return UtcNow.ToLocalTime(); } - set { UtcNow = value.ToUniversalTime(); } + this.Now = DateTime.Now; } - private object _lock = new object(); - private DateTime _utcNow; + public DateTime Now + { + get { return this.UtcNow.ToLocalTime(); } + set { this.UtcNow = value.ToUniversalTime(); } + } public DateTime UtcNow { get { - lock (_lock) + lock (this.mutex) { - return _utcNow; + return this.utcNow; } } + set { - lock (_lock) + lock (this.mutex) { - _utcNow = value; + this.utcNow = value; } } } - - public MockClock() - { - Now = DateTime.Now; - } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs index 7bc4e209..b6456e79 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs @@ -26,99 +26,103 @@ namespace FirebaseAdmin.Tests { - internal class MockHttpClientFactory : HttpClientFactory - { - private HttpMessageHandler Handler { get; set; } - - public MockHttpClientFactory(HttpMessageHandler handler) - { - Handler = handler; - } - - protected override HttpMessageHandler CreateHandler(CreateHttpClientArgs args) - { - return Handler; - } - } - /// /// An implementation that counts the number of requests - /// processed. + /// and facilitates mocking HTTP interactions locally. /// - internal abstract class CountableMessageHandler : HttpMessageHandler + internal class MockMessageHandler : CountableMessageHandler { - private int _calls; - - public int Calls - { - get { return _calls; } - } - - sealed protected override Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken) + public MockMessageHandler() { - Interlocked.Increment(ref _calls); - return SendAsyncCore(request, cancellationToken); + this.StatusCode = HttpStatusCode.OK; } - protected abstract Task SendAsyncCore( - HttpRequestMessage request, CancellationToken cancellationToken); - } + public delegate void SetHeaders(HttpResponseHeaders header); - /// - /// An implementation that counts the number of requests - /// and facilitates mocking HTTP interactions locally. - /// - internal class MockMessageHandler : CountableMessageHandler - { public string Request { get; private set; } - + public HttpStatusCode StatusCode { get; set; } - public Object Response { get; set; } - public delegate void SetHeaders(HttpResponseHeaders header); + public object Response { get; set; } public SetHeaders ApplyHeaders { get; set; } - public MockMessageHandler() - { - StatusCode = HttpStatusCode.OK; - } - protected override async Task SendAsyncCore( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Content != null) { - Request = await request.Content.ReadAsStringAsync(); + this.Request = await request.Content.ReadAsStringAsync(); } else { - Request = null; - } + this.Request = null; + } + var resp = new HttpResponseMessage(); string json; - if (Response is byte[]) + if (this.Response is byte[]) { - json = Encoding.UTF8.GetString(Response as byte[]); + json = Encoding.UTF8.GetString(this.Response as byte[]); } - else if (Response is string) + else if (this.Response is string) { - json = Response as string; + json = this.Response as string; } else { - json = NewtonsoftJsonSerializer.Instance.Serialize(Response); - } - resp.StatusCode = StatusCode; - if (ApplyHeaders != null) + json = NewtonsoftJsonSerializer.Instance.Serialize(this.Response); + } + + resp.StatusCode = this.StatusCode; + if (this.ApplyHeaders != null) { - ApplyHeaders(resp.Headers); + this.ApplyHeaders(resp.Headers); } + resp.Content = new StringContent(json, Encoding.UTF8, "application/json"); var tcs = new TaskCompletionSource(); tcs.SetResult(resp); return await tcs.Task; } } + + /// + /// An implementation that counts the number of requests + /// processed. + /// + internal abstract class CountableMessageHandler : HttpMessageHandler + { + private int calls; + + public int Calls + { + get { return this.calls; } + } + + protected sealed override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + Interlocked.Increment(ref this.calls); + return this.SendAsyncCore(request, cancellationToken); + } + + protected abstract Task SendAsyncCore( + HttpRequestMessage request, CancellationToken cancellationToken); + } + + internal class MockHttpClientFactory : HttpClientFactory + { + private readonly HttpMessageHandler handler; + + public MockHttpClientFactory(HttpMessageHandler handler) + { + this.handler = handler; + } + + protected override HttpMessageHandler CreateHandler(CreateHttpClientArgs args) + { + return this.handler; + } + } } diff --git a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs index e5ff1fbc..9d529ac5 100644 --- a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs +++ b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs @@ -26,35 +26,37 @@ namespace FirebaseAdmin public sealed class AppOptions { /// - /// used to authorize an app. All service calls made by - /// the app will be authorized using this. + /// Initializes a new instance of the class. + /// + public AppOptions() { } + + internal AppOptions(AppOptions options) + { + this.Credential = options.Credential; + this.ProjectId = options.ProjectId; + this.ServiceAccountId = options.ServiceAccountId; + } + + /// + /// Gets or sets the used to authorize an app. All service + /// calls made by the app will be authorized using this. /// public GoogleCredential Credential { get; set; } /// - /// The Google Cloud Platform project ID that should be associated with an app. + /// Gets or sets the Google Cloud Platform project ID that should be associated with an + /// app. /// public string ProjectId { get; set; } /// - /// The unique ID of the service account that should be associated with an app. + /// Gets or sets the unique ID of the service account that should be associated with an + /// app. /// This is used to /// create custom auth tokens when service account credentials are not available. The /// service account ID can be found in the client_email field of the service account /// JSON. /// public string ServiceAccountId { get; set; } - - /// - /// Creates a new instance. - /// - public AppOptions() {} - - internal AppOptions(AppOptions options) - { - Credential = options.Credential; - ProjectId = options.ProjectId; - ServiceAccountId = options.ServiceAccountId; - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index f01e2b7d..a07419db 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -23,24 +23,62 @@ namespace FirebaseAdmin.Auth /// This is the entry point to all server-side Firebase Authentication operations. You can /// get an instance of this class via FirebaseAuth.DefaultInstance. /// - public sealed class FirebaseAuth: IFirebaseService + public sealed class FirebaseAuth : IFirebaseService { - private readonly FirebaseApp _app; - private bool _deleted; - private readonly Lazy _tokenFactory; - private readonly Lazy _idTokenVerifier; - private readonly Lazy _userManager; - private readonly Object _lock = new Object(); + private readonly FirebaseApp app; + private readonly Lazy tokenFactory; + private readonly Lazy idTokenVerifier; + private readonly Lazy userManager; + private readonly object authLock = new object(); + private bool deleted; private FirebaseAuth(FirebaseApp app) { - _app = app; - _tokenFactory = new Lazy(() => - FirebaseTokenFactory.Create(_app), true); - _idTokenVerifier = new Lazy(() => - FirebaseTokenVerifier.CreateIDTokenVerifier(_app), true); - _userManager = new Lazy(() => - FirebaseUserManager.Create(_app)); + this.app = app; + this.tokenFactory = new Lazy( + () => FirebaseTokenFactory.Create(this.app), true); + this.idTokenVerifier = new Lazy( + () => FirebaseTokenVerifier.CreateIDTokenVerifier(this.app), true); + this.userManager = new Lazy(() => + FirebaseUserManager.Create(this.app)); + } + + /// + /// Gets the auth instance associated with the default Firebase app. This property is + /// null if the default app doesn't yet exist. + /// + public static FirebaseAuth DefaultInstance + { + get + { + var app = FirebaseApp.DefaultInstance; + if (app == null) + { + return null; + } + + return GetAuth(app); + } + } + + /// + /// Returns the auth instance for the specified app. + /// + /// The instance associated with the specified + /// app. + /// If the app argument is null. + /// An app instance. + public static FirebaseAuth GetAuth(FirebaseApp app) + { + if (app == null) + { + throw new ArgumentNullException("App argument must not be null."); + } + + return app.GetOrInit(typeof(FirebaseAuth).Name, () => + { + return new FirebaseAuth(app); + }); } /// @@ -76,7 +114,7 @@ private FirebaseAuth(FirebaseApp app) /// 128 characters. public async Task CreateCustomTokenAsync(string uid) { - return await CreateCustomTokenAsync(uid, default(CancellationToken)); + return await this.CreateCustomTokenAsync(uid, default(CancellationToken)); } /// @@ -115,7 +153,7 @@ public async Task CreateCustomTokenAsync(string uid) public async Task CreateCustomTokenAsync( string uid, CancellationToken cancellationToken) { - return await CreateCustomTokenAsync(uid, null, cancellationToken); + return await this.CreateCustomTokenAsync(uid, null, cancellationToken); } /// @@ -142,7 +180,7 @@ public async Task CreateCustomTokenAsync( public async Task CreateCustomTokenAsync( string uid, IDictionary developerClaims) { - return await CreateCustomTokenAsync(uid, developerClaims, default(CancellationToken)); + return await this.CreateCustomTokenAsync(uid, developerClaims, default(CancellationToken)); } /// @@ -174,14 +212,16 @@ public async Task CreateCustomTokenAsync( CancellationToken cancellationToken) { FirebaseTokenFactory tokenFactory; - lock (_lock) + lock (this.authLock) { - if (_deleted) + if (this.deleted) { throw new InvalidOperationException("Cannot invoke after deleting the app."); } - tokenFactory = _tokenFactory.Value; + + tokenFactory = this.tokenFactory.Value; } + return await tokenFactory.CreateCustomTokenAsync( uid, developerClaims, cancellationToken).ConfigureAwait(false); } @@ -204,7 +244,7 @@ public async Task CreateCustomTokenAsync( /// A Firebase ID token string to parse and verify. public async Task VerifyIdTokenAsync(string idToken) { - return await VerifyIdTokenAsync(idToken, default(CancellationToken)); + return await this.VerifyIdTokenAsync(idToken, default(CancellationToken)); } /// @@ -228,14 +268,15 @@ public async Task VerifyIdTokenAsync(string idToken) public async Task VerifyIdTokenAsync( string idToken, CancellationToken cancellationToken) { - lock (_lock) + lock (this.authLock) { - if (_deleted) + if (this.deleted) { throw new InvalidOperationException("Cannot invoke after deleting the app."); } } - return await _idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken) + + return await this.idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken) .ConfigureAwait(false); } @@ -244,6 +285,7 @@ public async Task VerifyIdTokenAsync( /// removes any claims currently set on the user account. The claims must serialize into /// a valid JSON string. The serialized claims must not be larger than 1000 characters. /// + /// A task that completes when the claims have been set. /// If is null, empty or longer /// than 128 characters. Or, if the serialized is larger than 1000 /// characters. @@ -255,9 +297,9 @@ public async Task VerifyIdTokenAsync( /// serialized claims should not be larger than 1000 characters. public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary claims) { - lock (_lock) + lock (this.authLock) { - if (_deleted) + if (this.deleted) { throw new InvalidOperationException("Cannot invoke after deleting the app."); } @@ -265,58 +307,30 @@ public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary + /// Deletes this service instance. + /// void IFirebaseService.Delete() { - lock (_lock) + lock (this.authLock) { - _deleted = true; - if (_tokenFactory.IsValueCreated) + this.deleted = true; + if (this.tokenFactory.IsValueCreated) { - _tokenFactory.Value.Dispose(); + this.tokenFactory.Value.Dispose(); } - } - } - /// - /// The auth instance associated with the default Firebase app. This property is - /// null if the default app doesn't yet exist. - /// - public static FirebaseAuth DefaultInstance - { - get - { - var app = FirebaseApp.DefaultInstance; - if (app == null) + if (this.userManager.IsValueCreated) { - return null; + this.userManager.Value.Dispose(); } - return GetAuth(app); } } - - /// - /// Returns the auth instance for the specified app. - /// - /// The instance associated with the specified - /// app. - /// If the app argument is null. - /// An app instance. - public static FirebaseAuth GetAuth(FirebaseApp app) - { - if (app == null) - { - throw new ArgumentNullException("App argument must not be null."); - } - return app.GetOrInit(typeof(FirebaseAuth).Name, () => - { - return new FirebaseAuth(app); - }); - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs index e9c0c7e3..6145e805 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs @@ -23,73 +23,54 @@ namespace FirebaseAdmin.Auth /// public sealed class FirebaseToken { + internal FirebaseToken(FirebaseTokenArgs args) + { + this.Issuer = args.Issuer; + this.Subject = args.Subject; + this.Audience = args.Audience; + this.ExpirationTimeSeconds = args.ExpirationTimeSeconds; + this.IssuedAtTimeSeconds = args.IssuedAtTimeSeconds; + this.Uid = args.Subject; + this.Claims = args.Claims; + } + /// - /// The issuer claim that identifies the principal that issued the JWT. + /// Gets the issuer claim that identifies the principal that issued the JWT. /// public string Issuer { get; private set; } /// - /// The subject claim identifying the principal that is the subject of the JWT. + /// Gets the subject claim identifying the principal that is the subject of the JWT. /// public string Subject { get; private set; } /// - /// The audience claim that identifies the audience that the JWT is intended for. + /// Gets the audience claim that identifies the audience that the JWT is intended for. /// public string Audience { get; private set; } /// - /// The expiration time claim that identifies the expiration time (in seconds) + /// Gets the expiration time claim that identifies the expiration time (in seconds) /// on or after which the token MUST NOT be accepted for processing. /// public long ExpirationTimeSeconds { get; private set; } /// - /// The issued at claim that identifies the time (in seconds) at which the JWT was issued. + /// Gets the issued at claim that identifies the time (in seconds) at which the JWT was + /// issued. /// public long IssuedAtTimeSeconds { get; private set; } - + /// - /// User ID of the user to which this ID token belongs. This is same as Subject. + /// Gets the User ID of the user to which this ID token belongs. This is same as + /// . /// public string Uid { get; private set; } /// - /// A read-only dictionary of all other claims present in the JWT. This can be used to + /// Gets all other claims present in the JWT as a readonly dictionary. This can be used to /// access custom claims of the token. /// public IReadOnlyDictionary Claims { get; private set; } - - internal FirebaseToken(FirebaseTokenArgs args) - { - Issuer = args.Issuer; - Subject = args.Subject; - Audience = args.Audience; - ExpirationTimeSeconds = args.ExpirationTimeSeconds; - IssuedAtTimeSeconds = args.IssuedAtTimeSeconds; - Uid = args.Subject; - Claims = args.Claims; - } - } - - internal sealed class FirebaseTokenArgs - { - [JsonProperty("iss")] - public string Issuer { get; set; } - - [JsonProperty("sub")] - public string Subject { get; set; } - - [JsonProperty("aud")] - public string Audience { get; set; } - - [JsonProperty("exp")] - public long ExpirationTimeSeconds { get; set; } - - [JsonProperty("iat")] - public long IssuedAtTimeSeconds { get; set; } - - [JsonIgnore] - public IReadOnlyDictionary Claims { get; set; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs new file mode 100644 index 00000000..027feb6b --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs @@ -0,0 +1,40 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Auth +{ + internal sealed class FirebaseTokenArgs + { + [JsonProperty("iss")] + public string Issuer { get; set; } + + [JsonProperty("sub")] + public string Subject { get; set; } + + [JsonProperty("aud")] + public string Audience { get; set; } + + [JsonProperty("exp")] + public long ExpirationTimeSeconds { get; set; } + + [JsonProperty("iat")] + public long IssuedAtTimeSeconds { get; set; } + + [JsonIgnore] + public IReadOnlyDictionary Claims { get; set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs index 79e3b4ab..411c97fa 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs @@ -15,46 +15,86 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; -using System.Threading.Tasks; -using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Google.Apis.Auth; -using Google.Apis.Http; using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; using Google.Apis.Util; -[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey="+ -"002400000480000094000000060200000024000052534131000400000100010081328559eaab41"+ -"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02"+ -"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597"+ -"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f"+ +[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" + +"002400000480000094000000060200000024000052534131000400000100010081328559eaab41" + +"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02" + +"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597" + +"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f" + "badddb9c")] namespace FirebaseAdmin.Auth { /// /// A helper class that creates Firebase custom tokens. /// - internal class FirebaseTokenFactory: IDisposable + internal class FirebaseTokenFactory : IDisposable { public const string FirebaseAudience = "https://identitytoolkit.googleapis.com/" + "google.identity.identitytoolkit.v1.IdentityToolkit"; - + public const int TokenDurationSeconds = 3600; 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" - ); + "acr", + "amr", + "at_hash", + "aud", + "auth_time", + "azp", + "cnf", + "c_hash", + "exp", + "firebase", + "iat", + "iss", + "jti", + "nbf", + "nonce", + "sub"); - private readonly ISigner _signer; - private readonly IClock _clock; + private readonly ISigner signer; + private readonly IClock clock; public FirebaseTokenFactory(ISigner signer, IClock clock) { - _signer = signer.ThrowIfNull(nameof(signer)); - _clock = clock.ThrowIfNull(nameof(clock)); + this.signer = signer.ThrowIfNull(nameof(signer)); + this.clock = clock.ThrowIfNull(nameof(clock)); + } + + public static FirebaseTokenFactory 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 = new IAMSigner(new HttpClientFactory(), app.Options.Credential); + } + else + { + // If a service account ID is specified, invoke the IAM service with it. + signer = new FixedAccountIAMSigner( + new HttpClientFactory(), app.Options.Credential, app.Options.ServiceAccountId); + } + + return new FirebaseTokenFactory(signer, SystemClock.Default); } public async Task CreateCustomTokenAsync( @@ -70,6 +110,7 @@ public async Task CreateCustomTokenAsync( { throw new ArgumentException("uid must not be longer than 128 characters"); } + if (developerClaims != null) { foreach (var entry in developerClaims) @@ -85,11 +126,11 @@ public async Task CreateCustomTokenAsync( var header = new JsonWebSignature.Header() { Algorithm = "RS256", - Type = "JWT" + Type = "JWT", }; - - var issued = (int)(_clock.UtcNow - UnixEpoch).TotalSeconds; - var keyId = await _signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false); + + var issued = (int)(this.clock.UtcNow - UnixEpoch).TotalSeconds; + var keyId = await this.signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false); var payload = new CustomTokenPayload() { Uid = uid, @@ -97,53 +138,30 @@ public async Task CreateCustomTokenAsync( Subject = keyId, Audience = FirebaseAudience, IssuedAtTimeSeconds = issued, - ExpirationTimeSeconds = issued + TokenDurationSeconds, + ExpirationTimeSeconds = issued + TokenDurationSeconds, }; + if (developerClaims != null && developerClaims.Count > 0) { payload.Claims = developerClaims; } + return await JwtUtils.CreateSignedJwtAsync( - header, payload, _signer, cancellationToken).ConfigureAwait(false); + header, payload, this.signer, cancellationToken).ConfigureAwait(false); } public void Dispose() { - _signer.Dispose(); + this.signer.Dispose(); } - public static FirebaseTokenFactory Create(FirebaseApp app) + internal class CustomTokenPayload : JsonWebToken.Payload { - 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 = new IAMSigner(new HttpClientFactory(), app.Options.Credential); - } - else - { - // If a service account ID is specified, invoke the IAM service with it. - signer = new FixedAccountIAMSigner( - new HttpClientFactory(), app.Options.Credential, app.Options.ServiceAccountId); - } - return new FirebaseTokenFactory(signer, SystemClock.Default); - } - } + [Newtonsoft.Json.JsonPropertyAttribute("uid")] + public string Uid { get; set; } - internal class CustomTokenPayload: JsonWebToken.Payload - { - [Newtonsoft.Json.JsonPropertyAttribute("uid")] - public string Uid { get; set; } - - [Newtonsoft.Json.JsonPropertyAttribute("claims")] - public IDictionary Claims { get; set; } + [Newtonsoft.Json.JsonPropertyAttribute("claims")] + public IDictionary Claims { get; set; } + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index e96b3349..172fa03e 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -34,7 +35,7 @@ internal sealed class FirebaseTokenVerifier private const string IdTokenCertUrl = "https://www.googleapis.com/robot/v1/metadata/x509/" + "securetoken@system.gserviceaccount.com"; - private const string FirebaseAudience ="https://identitytoolkit.googleapis.com/" + private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/" + "google.identity.identitytoolkit.v1.IdentityToolkit"; // See http://oid-info.com/get/2.16.840.1.101.3.4.2.1 @@ -43,117 +44,146 @@ internal sealed class FirebaseTokenVerifier private static readonly IReadOnlyList StandardClaims = ImmutableList.Create("iss", "aud", "exp", "iat", "sub", "uid"); - public string ProjectId { get; } - 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; - - public FirebaseTokenVerifier(FirebaseTokenVerifierArgs args) + 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; + + internal FirebaseTokenVerifier(FirebaseTokenVerifierArgs args) { - ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId)); - _shortName = args.ShortName.ThrowIfNullOrEmpty(nameof(args.ShortName)); - _operation = args.Operation.ThrowIfNullOrEmpty(nameof(args.Operation)); - _url = args.Url.ThrowIfNullOrEmpty(nameof(args.Url)); - _issuer = args.Issuer.ThrowIfNullOrEmpty(nameof(args.Issuer)); - _clock = args.Clock.ThrowIfNull(nameof(args.Clock)); - _keySource = args.PublicKeySource.ThrowIfNull(nameof(args.PublicKeySource)); - if ("aeiou".Contains(_shortName.ToLower().Substring(0, 1))) + 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.ThrowIfNull(nameof(args.Clock)); + this.keySource = args.PublicKeySource.ThrowIfNull(nameof(args.PublicKeySource)); + if ("aeiou".Contains(this.shortName.ToLower().Substring(0, 1))) { - _articledShortName = $"an {_shortName}"; + this.articledShortName = $"an {this.shortName}"; } else { - _articledShortName = $"a {_shortName}"; + this.articledShortName = $"a {this.shortName}"; } } - public async Task VerifyTokenAsync( + public string ProjectId { get; } + + internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app) + { + var projectId = app.GetProjectId(); + if (string.IsNullOrEmpty(projectId)) + { + throw new ArgumentException( + "Must initialize FirebaseApp with a project ID to verify ID tokens."); + } + + var keySource = new HttpPublicKeySource( + IdTokenCertUrl, SystemClock.Default, new HttpClientFactory()); + var args = new FirebaseTokenVerifierArgs() + { + ProjectId = projectId, + ShortName = "ID token", + Operation = "VerifyIdTokenAsync()", + Url = "https://firebase.google.com/docs/auth/admin/verify-id-tokens", + Issuer = "https://securetoken.google.com/", + Clock = SystemClock.Default, + PublicKeySource = keySource, + }; + + return new FirebaseTokenVerifier(args); + } + + internal async Task VerifyTokenAsync( string token, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(token)) { - throw new ArgumentException($"{_shortName} must not be null or empty."); + throw new ArgumentException($"{this.shortName} must not be null or empty."); } + string[] segments = token.Split('.'); if (segments.Length != 3) { - throw new FirebaseException($"Incorrect number of segments in ${_shortName}."); + throw new FirebaseException($"Incorrect number of segments in ${this.shortName}."); } var header = JwtUtils.Decode(segments[0]); var payload = JwtUtils.Decode(segments[1]); - var projectIdMessage = $"Make sure the {_shortName} comes from the same Firebase " + var projectIdMessage = $"Make sure the {this.shortName} comes from the same Firebase " + "project as the credential used to initialize this SDK."; - var verifyTokenMessage = $"See {_url} for details on how to retrieve a value " - + $"{_shortName}."; - var issuer = _issuer + ProjectId; + var verifyTokenMessage = $"See {this.url} for details on how to retrieve a value " + + $"{this.shortName}."; + var issuer = this.issuer + this.ProjectId; string error = null; if (string.IsNullOrEmpty(header.KeyId)) { - if (FirebaseAudience == payload.Audience) + if (payload.Audience == FirebaseAudience) { - error = $"{_operation} expects {_articledShortName}, but was given a custom " + error = $"{this.operation} expects {this.articledShortName}, but was given a custom " + "token."; } else if (header.Algorithm == "HS256") { - error = $"{_operation} expects {_articledShortName}, but was given a legacy " + error = $"{this.operation} expects {this.articledShortName}, but was given a legacy " + "custom token."; } else { - error = $"Firebase {_shortName} has no 'kid' claim."; + error = $"Firebase {this.shortName} has no 'kid' claim."; } } else if (header.Algorithm != "RS256") { - error = $"Firebase {_shortName} has incorrect algorithm. Expected RS256 but got " + error = $"Firebase {this.shortName} has incorrect algorithm. Expected RS256 but got " + $"{header.Algorithm}. {verifyTokenMessage}"; } - else if (ProjectId != payload.Audience) + else if (this.ProjectId != payload.Audience) { - error = $"{_shortName} has incorrect audience (aud) claim. Expected {ProjectId} " + error = $"{this.shortName} has incorrect audience (aud) claim. Expected {this.ProjectId} " + $"but got {payload.Audience}. {projectIdMessage} {verifyTokenMessage}"; } else if (payload.Issuer != issuer) { - error = $"{_shortName} has incorrect issuer (iss) claim. Expected {issuer} but " + error = $"{this.shortName} has incorrect issuer (iss) claim. Expected {issuer} but " + $"got {payload.Issuer}. {projectIdMessage} {verifyTokenMessage}"; } - else if (payload.IssuedAtTimeSeconds > _clock.UnixTimestamp()) + else if (payload.IssuedAtTimeSeconds > this.clock.UnixTimestamp()) { - error = $"Firebase {_shortName} issued at future timestamp"; + error = $"Firebase {this.shortName} issued at future timestamp"; } - else if (payload.ExpirationTimeSeconds < _clock.UnixTimestamp()) + else if (payload.ExpirationTimeSeconds < this.clock.UnixTimestamp()) { - error = $"Firebase {_shortName} expired at {payload.ExpirationTimeSeconds}"; + error = $"Firebase {this.shortName} expired at {payload.ExpirationTimeSeconds}"; } else if (string.IsNullOrEmpty(payload.Subject)) { - error = $"Firebase {_shortName} has no or empty subject (sub) claim."; + error = $"Firebase {this.shortName} has no or empty subject (sub) claim."; } else if (payload.Subject.Length > 128) { - error = $"Firebase {_shortName} has a subject claim longer than 128 characters."; + error = $"Firebase {this.shortName} has a subject claim longer than 128 characters."; } - + if (error != null) { throw new FirebaseException(error); } - await VerifySignatureAsync(segments, header.KeyId, cancellationToken) + 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); } @@ -162,6 +192,14 @@ await VerifySignatureAsync(segments, header.KeyId, cancellationToken) /// 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) { @@ -171,15 +209,16 @@ private async Task VerifySignatureAsync( hash = hashAlg.ComputeHash( Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}")); } + var signature = JwtUtils.Base64DecodeToBytes(segments[2]); - var keys = await _keySource.GetPublicKeysAsync(cancellationToken) + var keys = await this.keySource.GetPublicKeysAsync(cancellationToken) .ConfigureAwait(false); var verified = keys.Any(key => #if NETSTANDARD1_5 || NETSTANDARD2_0 key.Id == keyId && key.RSA.VerifyHash( hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) #elif NET45 - key.Id == keyId && + key.Id == keyId && ((RSACryptoServiceProvider) key.RSA).VerifyHash(hash, Sha256Oid, signature) #else #error Unsupported target @@ -187,42 +226,8 @@ private async Task VerifySignatureAsync( ); if (!verified) { - throw new FirebaseException($"Failed to verify {_shortName} signature."); + throw new FirebaseException($"Failed to verify {this.shortName} signature."); } } - - internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app) - { - var projectId = app.GetProjectId(); - if (string.IsNullOrEmpty(projectId)) - { - throw new ArgumentException( - "Must initialize FirebaseApp with a project ID to verify ID tokens."); - } - var keySource = new HttpPublicKeySource( - IdTokenCertUrl, SystemClock.Default, new HttpClientFactory()); - var args = new FirebaseTokenVerifierArgs() - { - ProjectId = projectId, - ShortName = "ID token", - Operation = "VerifyIdTokenAsync()", - Url = "https://firebase.google.com/docs/auth/admin/verify-id-tokens", - Issuer = "https://securetoken.google.com/", - Clock = SystemClock.Default, - PublicKeySource = keySource, - }; - return new FirebaseTokenVerifier(args); - } - } - - internal sealed class FirebaseTokenVerifierArgs - { - public string ProjectId { get; set; } - public string ShortName { get; set; } - public string Operation { get; set; } - public string Url { get; set; } - public string Issuer { get; set; } - public IClock Clock { get; set; } - public IPublicKeySource PublicKeySource { get; set; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs new file mode 100644 index 00000000..d79524b7 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs @@ -0,0 +1,35 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Google.Apis.Util; + +namespace FirebaseAdmin.Auth +{ + internal sealed class FirebaseTokenVerifierArgs + { + public string ProjectId { get; set; } + + public string ShortName { get; set; } + + public string Operation { get; set; } + + public string Url { get; set; } + + public string Issuer { get; set; } + + public IClock Clock { get; set; } + + public IPublicKeySource PublicKeySource { get; set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs index 3a3e86fc..805d6347 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs @@ -12,32 +12,51 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Google.Apis.Auth.OAuth2; -using Google.Apis.Http; -using Newtonsoft.Json.Linq; using System; using System.Net.Http; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; +using Newtonsoft.Json.Linq; namespace FirebaseAdmin.Auth { /// - /// FirebaseUserManager provides methods for interacting with the + /// FirebaseUserManager provides methods for interacting with the /// - /// Google Identity Toolkit via its REST API. This class does not hold any mutable state, + /// Google Identity Toolkit via its REST API. This class does not hold any mutable state, /// and is thread safe. /// internal class FirebaseUserManager : IDisposable { - private const string ID_TOOLKIT_URL = "https://identitytoolkit.googleapis.com/v1/projects/{0}"; + private const string IdTooklitUrl = "https://identitytoolkit.googleapis.com/v1/projects/{0}"; - private readonly ConfigurableHttpClient _httpClient; - private readonly string _baseUrl; + private readonly ConfigurableHttpClient httpClient; + private readonly string baseUrl; internal FirebaseUserManager(FirebaseUserManagerArgs args) { - _httpClient = args.ClientFactory.CreateAuthorizedHttpClient(args.Credential); - _baseUrl = string.Format(ID_TOOLKIT_URL, args.ProjectId); + this.httpClient = args.ClientFactory.CreateAuthorizedHttpClient(args.Credential); + this.baseUrl = string.Format(IdTooklitUrl, args.ProjectId); + } + + public static FirebaseUserManager Create(FirebaseApp app) + { + var projectId = app.GetProjectId(); + if (string.IsNullOrEmpty(projectId)) + { + throw new ArgumentException( + "Must initialize FirebaseApp with a project ID to manage users."); + } + + var args = new FirebaseUserManagerArgs + { + ClientFactory = new HttpClientFactory(), + Credential = app.Options.Credential, + ProjectId = projectId, + }; + + return new FirebaseUserManager(args); } /// @@ -48,7 +67,7 @@ internal FirebaseUserManager(FirebaseUserManagerArgs args) public async Task UpdateUserAsync(UserRecord user) { var updatePath = "/accounts:update"; - var resopnse = await PostAsync(updatePath, user); + var resopnse = await this.PostAsync(updatePath, user); try { @@ -64,13 +83,18 @@ public async Task UpdateUserAsync(UserRecord user) } } + public void Dispose() + { + this.httpClient.Dispose(); + } + private async Task PostAsync(string path, UserRecord user) { - var requestUri = $"{_baseUrl}{path}"; + var requestUri = $"{this.baseUrl}{path}"; HttpResponseMessage response = null; try { - response = await _httpClient.PostJsonAsync(requestUri, user, default); + response = await this.httpClient.PostJsonAsync(requestUri, user, default); var json = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) @@ -90,36 +114,5 @@ private async Task PostAsync(string path, UserRecord user) throw new FirebaseException("Error while calling Firebase Auth service", e); } } - - internal static FirebaseUserManager Create(FirebaseApp app) - { - var projectId = app.GetProjectId(); - if (string.IsNullOrEmpty(projectId)) - { - throw new ArgumentException( - "Must initialize FirebaseApp with a project ID to manage users."); - } - - var args = new FirebaseUserManagerArgs - { - ClientFactory = new HttpClientFactory(), - Credential = app.Options.Credential, - ProjectId = projectId, - }; - - return new FirebaseUserManager(args); - } - - public void Dispose() - { - _httpClient.Dispose(); - } - } - - internal sealed class FirebaseUserManagerArgs - { - public HttpClientFactory ClientFactory { get; set; } - public GoogleCredential Credential { get; set; } - public string ProjectId { get; set; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManagerArgs.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManagerArgs.cs new file mode 100644 index 00000000..034f4ffc --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManagerArgs.cs @@ -0,0 +1,28 @@ +// Copyright 2019, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; + +namespace FirebaseAdmin.Auth +{ + internal sealed class FirebaseUserManagerArgs + { + public HttpClientFactory ClientFactory { get; set; } + + public GoogleCredential Credential { get; set; } + + public string ProjectId { get; set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs new file mode 100644 index 00000000..8398d9fb --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs @@ -0,0 +1,45 @@ +// Copyright 2018, Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; +using Google.Apis.Util; + +namespace FirebaseAdmin.Auth +{ + /// + /// An implementation that uses the IAM service to sign data. Unlike + /// this class does not attempt to auto discover a service account ID. + /// Insterad it must be initialized with a fixed service account ID string. + /// + internal sealed class FixedAccountIAMSigner : IAMSigner + { + private readonly string keyId; + + public FixedAccountIAMSigner( + HttpClientFactory clientFactory, GoogleCredential credential, string keyId) + : base(clientFactory, credential) + { + this.keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId)); + } + + public override Task GetKeyIdAsync( + CancellationToken cancellationToken = default(CancellationToken)) + { + return Task.FromResult(this.keyId); + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs index ffbbd9ff..6851d190 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs @@ -21,8 +21,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Google.Apis.Json; using Google.Apis.Http; +using Google.Apis.Json; using Google.Apis.Util; #if NETSTANDARD1_5 || NETSTANDARD2_0 @@ -40,7 +40,7 @@ namespace FirebaseAdmin.Auth /// HTTP server. Retrieved keys are cached in memory according to the HTTP cache-control /// directive. /// - internal sealed class HttpPublicKeySource: IPublicKeySource + internal sealed class HttpPublicKeySource : IPublicKeySource { // Default clock skew used by most GCP libraries. This interval is subtracted from the // cache expiry time, before any expiration checks. This helps correct for minor @@ -48,44 +48,44 @@ internal sealed class HttpPublicKeySource: IPublicKeySource // pre-emptively refreshed instead of waiting until the last second. private static readonly TimeSpan ClockSkew = new TimeSpan(hours: 0, minutes: 5, seconds: 0); - private readonly string _certUrl; - private IReadOnlyList _cachedKeys; - private DateTime _expirationTime; - private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - private readonly IClock _clock; - private readonly HttpClientFactory _clientFactory; + private readonly SemaphoreSlim cacheLock = new SemaphoreSlim(1, 1); + private readonly string certUrl; + private readonly IClock clock; + private readonly HttpClientFactory clientFactory; + private DateTime expirationTime; + private IReadOnlyList cachedKeys; public HttpPublicKeySource(string certUrl, IClock clock, HttpClientFactory clientFactory) { - _certUrl = certUrl.ThrowIfNullOrEmpty(nameof(certUrl)); - _clock = clock.ThrowIfNull(nameof(clock)); - _clientFactory = clientFactory.ThrowIfNull(nameof(clientFactory)); - _expirationTime = clock.UtcNow; + this.certUrl = certUrl.ThrowIfNullOrEmpty(nameof(certUrl)); + this.clock = clock.ThrowIfNull(nameof(clock)); + this.clientFactory = clientFactory.ThrowIfNull(nameof(clientFactory)); + this.expirationTime = clock.UtcNow; } public async Task> GetPublicKeysAsync( CancellationToken cancellationToken = default(CancellationToken)) { - if (_cachedKeys == null || _clock.UtcNow >= _expirationTime) + if (this.cachedKeys == null || this.clock.UtcNow >= this.expirationTime) { - await _lock.WaitAsync(cancellationToken).ConfigureAwait(false); + await this.cacheLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { - var now = _clock.UtcNow; - if (_cachedKeys == null || now >= _expirationTime) + var now = this.clock.UtcNow; + if (this.cachedKeys == null || now >= this.expirationTime) { - using (var httpClient = _clientFactory.CreateDefaultHttpClient()) + using (var httpClient = this.clientFactory.CreateDefaultHttpClient()) { - var response = await httpClient.GetAsync(_certUrl, cancellationToken) + var response = await httpClient.GetAsync(this.certUrl, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); - _cachedKeys = ParseKeys(await response.Content.ReadAsStringAsync() + this.cachedKeys = this.ParseKeys(await response.Content.ReadAsStringAsync() .ConfigureAwait(false)); var cacheControl = response.Headers.CacheControl; if (cacheControl?.MaxAge != null) { - _expirationTime = now.Add(cacheControl.MaxAge.Value) + this.expirationTime = now.Add(cacheControl.MaxAge.Value) .Subtract(ClockSkew); } } @@ -97,11 +97,11 @@ public async Task> GetPublicKeysAsync( } finally { - _lock.Release(); + this.cacheLock.Release(); } } - return _cachedKeys; + return this.cachedKeys; } private IReadOnlyList ParseKeys(string json) @@ -112,6 +112,7 @@ private IReadOnlyList ParseKeys(string json) { throw new InvalidDataException("No public keys present in the response."); } + var builder = ImmutableList.CreateBuilder(); foreach (var entry in rawKeys) { @@ -126,6 +127,7 @@ private IReadOnlyList ParseKeys(string json) #endif builder.Add(new PublicKey(entry.Key, rsa)); } + return builder.ToImmutableList(); } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs index dec55cd4..f7d810bd 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs @@ -36,16 +36,17 @@ internal class IAMSigner : ISigner { private const string SignBlobUrl = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/{0}:signBlob"; - private const string MetadataServerUrl = + + private const string MetadataServerUrl = "http://metadata/computeMetadata/v1/instance/service-accounts/default/email"; - private readonly ConfigurableHttpClient _httpClient; - private readonly Lazy> _keyId; + private readonly ConfigurableHttpClient httpClient; + private readonly Lazy> keyId; public IAMSigner(HttpClientFactory clientFactory, GoogleCredential credential) { - _httpClient = clientFactory.CreateAuthorizedHttpClient(credential); - _keyId = new Lazy>( + this.httpClient = clientFactory.CreateAuthorizedHttpClient(credential); + this.keyId = new Lazy>( async () => await DiscoverServiceAccountIdAsync(clientFactory) .ConfigureAwait(false), true); } @@ -53,18 +54,19 @@ public IAMSigner(HttpClientFactory clientFactory, GoogleCredential credential) public async Task SignDataAsync( byte[] data, CancellationToken cancellationToken = default(CancellationToken)) { - var keyId = await GetKeyIdAsync(cancellationToken).ConfigureAwait(false); + var keyId = await this.GetKeyIdAsync(cancellationToken).ConfigureAwait(false); var url = string.Format(SignBlobUrl, keyId); var request = new SignBlobRequest { BytesToSign = Convert.ToBase64String(data), }; + try { - var response = await _httpClient.PostJsonAsync(url, request, cancellationToken) + var response = await this.httpClient.PostJsonAsync(url, request, cancellationToken) .ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - ThrowIfError(response, json); + this.ThrowIfError(response, json); var parsed = NewtonsoftJsonSerializer.Instance.Deserialize(json); return Convert.FromBase64String(parsed.Signature); } @@ -74,34 +76,12 @@ public async Task SignDataAsync( } } - private void ThrowIfError(HttpResponseMessage response, string content) - { - if (response.IsSuccessStatusCode) - { - return; - } - string error = null; - try - { - var result = NewtonsoftJsonSerializer.Instance.Deserialize(content); - error = result?.Error.Message; - } - catch (Exception) {} // Ignore any errors encountered while parsing the originl error. - if (string.IsNullOrEmpty(error)) - { - error = "Response status code does not indicate success: " - + $"{(int) response.StatusCode} ({response.StatusCode})" - + $"{Environment.NewLine}{content}"; - } - throw new FirebaseException(error); - } - public virtual async Task GetKeyIdAsync( CancellationToken cancellationToken = default(CancellationToken)) { try { - return await _keyId.Value.ConfigureAwait(false); + return await this.keyId.Value.ConfigureAwait(false); } catch (Exception e) { @@ -114,6 +94,11 @@ public virtual async Task GetKeyIdAsync( } } + public void Dispose() + { + this.httpClient.Dispose(); + } + private static async Task DiscoverServiceAccountIdAsync( HttpClientFactory clientFactory) { @@ -124,67 +109,68 @@ private static async Task DiscoverServiceAccountIdAsync( } } - public void Dispose() + private void ThrowIfError(HttpResponseMessage response, string content) { - _httpClient.Dispose(); - } - } + if (response.IsSuccessStatusCode) + { + return; + } - /// - /// Represents the sign request sent to the remote IAM service. - /// - internal class SignBlobRequest - { - [Newtonsoft.Json.JsonProperty("bytesToSign")] - public string BytesToSign { get; set; } - } + string error = null; + try + { + var result = NewtonsoftJsonSerializer.Instance.Deserialize(content); + error = result?.Error.Message; + } + catch (Exception) + { + // Ignore any errors encountered while parsing the originl error. + } - /// - /// Represents the sign response sent by the remote IAM service. - /// - internal class SignBlobResponse - { - [Newtonsoft.Json.JsonProperty("signature")] - public string Signature { get; set; } - } + if (string.IsNullOrEmpty(error)) + { + error = "Response status code does not indicate success: " + + $"{(int)response.StatusCode} ({response.StatusCode})" + + $"{Environment.NewLine}{content}"; + } - /// - /// Represents an error response sent by the remote IAM service. - /// - internal class SignBlobError - { - [Newtonsoft.Json.JsonProperty("error")] - public SignBlobErrorDetail Error { get; set; } - } + throw new FirebaseException(error); + } - /// - /// Represents the error details embedded in an IAM error response. - /// - internal class SignBlobErrorDetail - { - [Newtonsoft.Json.JsonProperty("message")] - public string Message { get; set; } - } + /// + /// Represents the sign request sent to the remote IAM service. + /// + internal class SignBlobRequest + { + [Newtonsoft.Json.JsonProperty("bytesToSign")] + public string BytesToSign { get; set; } + } - /// - /// An implementation that uses the IAM service to sign data. Unlike - /// this class does not attempt to auto discover a service account ID. - /// Insterad it must be initialized with a fixed service account ID string. - /// - internal sealed class FixedAccountIAMSigner : IAMSigner - { - private readonly string _keyId; + /// + /// Represents the sign response sent by the remote IAM service. + /// + internal class SignBlobResponse + { + [Newtonsoft.Json.JsonProperty("signature")] + public string Signature { get; set; } + } - public FixedAccountIAMSigner(HttpClientFactory clientFactory, GoogleCredential credential, - string keyId): base(clientFactory, credential) + /// + /// Represents an error response sent by the remote IAM service. + /// + private class SignBlobError { - _keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId)); + [Newtonsoft.Json.JsonProperty("error")] + public SignBlobErrorDetail Error { get; set; } } - public override Task GetKeyIdAsync( - CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Represents the error details embedded in an IAM error response. + /// + private class SignBlobErrorDetail { - return Task.FromResult(_keyId); + [Newtonsoft.Json.JsonProperty("message")] + public string Message { get; set; } } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs index 2d63798e..dcacb8ee 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs @@ -22,7 +22,7 @@ namespace FirebaseAdmin.Auth /// Represents an object can be used to cryptographically sign data. Mainly used for signing /// custom JWT tokens issued to Firebase users. /// - internal interface ISigner: IDisposable + internal interface ISigner : IDisposable { /// /// Returns the ID (client email) of the service account used to sign payloads. diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs index 7fb5ab74..b252236f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs @@ -25,12 +25,6 @@ namespace FirebaseAdmin.Auth /// internal static class JwtUtils { - private static string Encode(object obj) - { - var json = NewtonsoftJsonSerializer.Instance.Serialize(obj); - return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(json)); - } - /// /// Decodes a single JWT segment, and deserializes it into a value of type /// . @@ -44,19 +38,13 @@ public static T Decode(string value) return NewtonsoftJsonSerializer.Instance.Deserialize(json); } - private static string UrlSafeBase64Encode(byte[] bytes) - { - var base64Value = Convert.ToBase64String(bytes); - return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_'); - } - - public static string Base64Decode(string input) + internal static string Base64Decode(string input) { var raw = Base64DecodeToBytes(input); return Encoding.UTF8.GetString(raw); } - public static byte[] Base64DecodeToBytes(string input) + internal static byte[] Base64DecodeToBytes(string input) { // undo the url safe replacements input = input.Replace('-', '+').Replace('_', '/'); @@ -65,11 +53,14 @@ public static byte[] Base64DecodeToBytes(string input) case 2: input += "=="; break; case 3: input += "="; break; } + return Convert.FromBase64String(input); } - public static async Task CreateSignedJwtAsync( - object header, object payload, ISigner signer, + internal static async Task CreateSignedJwtAsync( + object header, + object payload, + ISigner signer, CancellationToken cancellationToken = default(CancellationToken)) { string encodedHeader = Encode(header); @@ -85,5 +76,17 @@ public static async Task CreateSignedJwtAsync( assertion.Append('.').Append(UrlSafeBase64Encode(signature)); return assertion.ToString(); } + + private static string Encode(object obj) + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(obj); + return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(json)); + } + + private static string UrlSafeBase64Encode(byte[] bytes) + { + var base64Value = Convert.ToBase64String(bytes); + return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_'); + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs index e1779633..483f5f7c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs @@ -27,21 +27,21 @@ namespace FirebaseAdmin.Auth /// internal sealed class PublicKey { + public PublicKey(string keyId, RSAKey rsa) + { + this.Id = keyId; + this.RSA = rsa; + } + /// - /// The unique identifier of this key. + /// Gets the unique identifier of this key. /// public string Id { get; } /// - /// A instance containing the contents of + /// Gets the instance containing the contents of /// the public key. /// public RSAKey RSA { get; } - - public PublicKey(string keyId, RSAKey rsa) - { - Id = keyId; - RSA = rsa; - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs index adfc064d..79b782e7 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs @@ -16,6 +16,7 @@ using System.Threading; using System.Threading.Tasks; using Google.Apis.Auth.OAuth2; +using Google.Apis.Util; namespace FirebaseAdmin.Auth { @@ -23,31 +24,27 @@ namespace FirebaseAdmin.Auth /// An implementation that uses service account credentials to sign /// data. Uses the private key present in the credential to produce signatures. /// - internal sealed class ServiceAccountSigner: ISigner + internal sealed class ServiceAccountSigner : ISigner { - private readonly ServiceAccountCredential _credential; + private readonly ServiceAccountCredential credential; public ServiceAccountSigner(ServiceAccountCredential credential) { - if (credential == null) - { - throw new ArgumentNullException("Credential must not be null."); - } - _credential = credential; + this.credential = credential.ThrowIfNull(nameof(credential)); } public Task GetKeyIdAsync(CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(_credential.Id); + return Task.FromResult(this.credential.Id); } public Task SignDataAsync(byte[] data, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); - var signature = _credential.CreateSignature(data); + var signature = this.credential.CreateSignature(data); return Task.FromResult(Convert.FromBase64String(signature)); } - public void Dispose() {} + public void Dispose() { } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs b/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs index 69505630..ec584e87 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/UserRecord.cs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Google.Apis.Json; -using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Text; +using Google.Apis.Json; +using Newtonsoft.Json; namespace FirebaseAdmin.Auth { @@ -26,49 +26,49 @@ namespace FirebaseAdmin.Auth /// internal class UserRecord { - private string _uid; - private IReadOnlyDictionary _customClaims; + private string uid; + private IReadOnlyDictionary customClaims; + + public UserRecord(string uid) + { + this.Uid = uid; + } /// - /// The user ID of this user. + /// Gets the user ID of this user. /// [JsonProperty("localId")] public string Uid { - get => _uid; + get => this.uid; private set { CheckUid(value); - _uid = value; + this.uid = value; } } /// - /// Returns custom claims set on this user. + /// Gets or sets the custom claims set on this user. /// [JsonIgnore] public IReadOnlyDictionary CustomClaims { - get => _customClaims; + get => this.customClaims; set { CheckCustomClaims(value); - _customClaims = value; + this.customClaims = value; } } [JsonProperty("customAttributes")] internal string CustomClaimsString => SerializeClaims(CustomClaims); - public UserRecord(string uid) - { - Uid = uid; - } - /// /// Checks if the given user ID is valid. /// - /// The user ID. Must not be null or longer than + /// The user ID. Must not be null or longer than /// 128 characters. public static void CheckUid(string uid) { @@ -85,10 +85,10 @@ public static void CheckUid(string uid) /// /// Checks if the given set of custom claims are valid. /// - /// The custom claims. Claim names must + /// The custom claims. Claim names must /// not be null or empty and must not be reserved and the serialized /// claims have to be less than 1000 bytes. - public static void CheckCustomClaims(IReadOnlyDictionary customClaims) + internal static void CheckCustomClaims(IReadOnlyDictionary customClaims) { if (customClaims == null) { @@ -101,6 +101,7 @@ public static void CheckCustomClaims(IReadOnlyDictionary customC { throw new ArgumentException("Claim names must not be null or empty"); } + if (FirebaseTokenFactory.ReservedClaims.Contains(key)) { throw new ArgumentException($"Claim {key} is reserved and cannot be set"); diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index db5a2a02..650db9df 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -33,21 +33,28 @@ internal static class Extensions /// . Returns null if the GoogleCredential is not /// based on a service account. /// + /// A service account credential if available, or null. + /// The Google credential from which to extract service account + /// credentials. public static ServiceAccountCredential ToServiceAccountCredential( this GoogleCredential credential) { if (credential.UnderlyingCredential is GoogleCredential) { - return ((GoogleCredential) credential.UnderlyingCredential) + return ((GoogleCredential)credential.UnderlyingCredential) .ToServiceAccountCredential(); } + return credential.UnderlyingCredential as ServiceAccountCredential; } /// /// Creates a default (unauthenticated) from the /// factory. - /// + /// + /// An HTTP client that can be used to make unauthenticated requests. + /// The used to create + /// the HTTP client. public static ConfigurableHttpClient CreateDefaultHttpClient( this HttpClientFactory clientFactory) { @@ -58,6 +65,11 @@ public static ConfigurableHttpClient CreateDefaultHttpClient( /// Creates an authenticated from the /// factory. /// + /// An HTTP client that can be used to OAuth2 authorized requests. + /// The used to create + /// the HTTP client. + /// The Google credential that will be used to authenticate + /// outgoing HTTP requests. public static ConfigurableHttpClient CreateAuthorizedHttpClient( this HttpClientFactory clientFactory, GoogleCredential credential) { @@ -69,6 +81,14 @@ public static ConfigurableHttpClient CreateAuthorizedHttpClient( /// /// Makes a JSON POST request using the given parameters. /// + /// An representing the response to the + /// POST request. + /// Type of the object that will be serialized into JSON. + /// The used to make the request. + /// URI for the outgoing request. + /// The object that will be serialized as the JSON body. + /// A cancellation token to monitor the asynchronous + /// operation. public static async Task PostJsonAsync( this HttpClient client, string requestUri, T body, CancellationToken cancellationToken) { @@ -81,9 +101,12 @@ public static async Task PostJsonAsync( /// /// Returns a Unix-styled timestamp (seconds from epoch) from the . /// + /// Number of seconds since epoch. + /// The used to generate the timestamp. public static long UnixTimestamp(this IClock clock) { - return (long) (clock.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + var timeSinceEpoch = clock.UtcNow.Subtract(new DateTime(1970, 1, 1)); + return (long)timeSinceEpoch.TotalSeconds; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj index e5a8a453..7ecd116f 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj @@ -21,11 +21,16 @@ https://github.com/Firebase/firebase-admin-dotnet git https://github.com/Firebase/firebase-admin-dotnet + ../../stylecop.ruleset + + all + + diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index 0c06aed3..9f12ecd2 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -18,18 +18,19 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using Google; -using Google.Apis.Logging; using Google.Apis.Auth.OAuth2; +using Google.Apis.Logging; -[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey="+ -"002400000480000094000000060200000024000052534131000400000100010081328559eaab41"+ -"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02"+ -"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597"+ -"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f"+ +[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" + +"002400000480000094000000060200000024000052534131000400000100010081328559eaab41" + +"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02" + +"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597" + +"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f" + "badddb9c")] -namespace FirebaseAdmin +namespace FirebaseAdmin { - internal delegate TResult ServiceFactory() where TResult: IFirebaseService; + internal delegate TResult ServiceFactory() + where TResult : IFirebaseService; /// /// This is the entry point to the Firebase Admin SDK. It holds configuration and state common @@ -40,138 +41,97 @@ namespace FirebaseAdmin /// public sealed class FirebaseApp { - private const string DefaultAppName = "[DEFAULT]"; - internal static readonly IReadOnlyList DefaultScopes = ImmutableList.Create( - // Enables access to Firebase Realtime Database. - "https://www.googleapis.com/auth/firebase", - - // Enables access to the email address associated with a project. - "https://www.googleapis.com/auth/userinfo.email", - - // Enables access to Google Identity Toolkit (for user management APIs). - "https://www.googleapis.com/auth/identitytoolkit", + "https://www.googleapis.com/auth/firebase", // RTDB. + "https://www.googleapis.com/auth/userinfo.email", // RTDB + "https://www.googleapis.com/auth/identitytoolkit", // User management + "https://www.googleapis.com/auth/devstorage.full_control", // Cloud Storage + "https://www.googleapis.com/auth/cloud-platform", // Cloud Firestore + "https://www.googleapis.com/auth/datastore"); - // Enables access to Google Cloud Storage. - "https://www.googleapis.com/auth/devstorage.full_control", - - // Enables access to Google Cloud Firestore - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/datastore" - ); + private const string DefaultAppName = "[DEFAULT]"; private static readonly Dictionary Apps = new Dictionary(); private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); // Guards the mutable state local to an app instance. - private readonly Object _lock = new Object(); - private bool _deleted = false; - private readonly AppOptions _options; - - /// - /// A copy of the this app was created with. - /// - public AppOptions Options - { - get - { - return new AppOptions(_options); - } - } - - /// - /// Name of this app. - /// - public string Name { get; } + private readonly object appLock = new object(); + private readonly AppOptions options; // A collection of stateful services initialized using this app instance (e.g. // FirebaseAuth). Services are tracked here so they can be cleaned up when the app is // deleted. - private readonly Dictionary _services = new Dictionary(); + private readonly Dictionary services = new Dictionary(); + private bool deleted = false; private FirebaseApp(AppOptions options, string name) { - _options = new AppOptions(options); - if (_options.Credential == null) + this.options = new AppOptions(options); + if (this.options.Credential == null) { throw new ArgumentNullException("Credential must be set"); } - if (_options.Credential.IsCreateScopedRequired) + + if (this.options.Credential.IsCreateScopedRequired) { - _options.Credential = _options.Credential.CreateScoped(DefaultScopes); + this.options.Credential = this.options.Credential.CreateScoped(DefaultScopes); } - Name = name; + + this.Name = name; } /// - /// Deletes this app instance and cleans up any state associated with it. Once an app has - /// been deleted, accessing any services related to it will result in an exception. - /// If the app is already deleted, this method is a no-op. + /// Gets the default app instance. This property is null if the default app instance + /// doesn't yet exist. /// - public void Delete() + public static FirebaseApp DefaultInstance { - // Clean up local state - lock (_lock) - { - _deleted = true; - foreach (var entry in _services) - { - try - { - entry.Value.Delete(); - } - catch (Exception e) - { - Logger.Error(e, "Error while cleaning up service {0}", entry.Key); - } - } - _services.Clear(); - } - // Clean up global state - lock (Apps) + get { - Apps.Remove(Name); + return GetInstance(DefaultAppName); } } - internal T GetOrInit(string id, ServiceFactory initializer) where T : class, IFirebaseService + /// + /// Gets a copy of the this app was created with. + /// + public AppOptions Options { - lock (_lock) + get { - if (_deleted) - { - throw new InvalidOperationException("Cannot use an app after it has been deleted"); - } - IFirebaseService service; - if (!_services.TryGetValue(id, out service)) - { - service = initializer(); - _services.Add(id, service); - } - return (T) service; + return new AppOptions(this.options); } } - internal string GetProjectId() + /// + /// Gets the name of this app. + /// + public string Name { get; } + + /// + /// Returns the app instance identified by the given name. + /// + /// The instance with the specified name or null if it + /// doesn't exist. + /// If the name argument is null or empty. + /// Name of the app to retrieve. + public static FirebaseApp GetInstance(string name) { - if (!string.IsNullOrEmpty(Options.ProjectId)) - { - return Options.ProjectId; - } - var projectId = Options.Credential.ToServiceAccountCredential()?.ProjectId; - if (!String.IsNullOrEmpty(projectId)) + if (string.IsNullOrEmpty(name)) { - return projectId; + throw new ArgumentException("App name to lookup must not be null or empty"); } - foreach (var variableName in new [] {"GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"}) + + lock (Apps) { - projectId = Environment.GetEnvironmentVariable(variableName); - if (!String.IsNullOrEmpty(projectId)) + FirebaseApp app; + if (Apps.TryGetValue(name, out app)) { - return projectId; + return app; } } + return null; } @@ -226,6 +186,7 @@ public static FirebaseApp Create(AppOptions options, string name) { throw new ArgumentException("App name must not be null or empty"); } + options = options ?? GetOptionsFromEnvironment(); lock (Apps) { @@ -235,59 +196,49 @@ public static FirebaseApp Create(AppOptions options, string name) { throw new ArgumentException("The default FirebaseApp already exists."); } - else + else { throw new ArgumentException($"FirebaseApp named {name} already exists."); } } + var app = new FirebaseApp(options, name); Apps.Add(name, app); return app; } } - private static AppOptions GetOptionsFromEnvironment() - { - return new AppOptions() - { - Credential = GoogleCredential.GetApplicationDefault(), - }; - } - /// - /// The default app instance. This property is null if the default app instance - /// doesn't yet exist. + /// Deletes this app instance and cleans up any state associated with it. Once an app has + /// been deleted, accessing any services related to it will result in an exception. + /// If the app is already deleted, this method is a no-op. /// - public static FirebaseApp DefaultInstance + public void Delete() { - get + // Clean up local state + lock (this.appLock) { - return GetInstance(DefaultAppName); - } - } + this.deleted = true; + foreach (var entry in this.services) + { + try + { + entry.Value.Delete(); + } + catch (Exception e) + { + Logger.Error(e, "Error while cleaning up service {0}", entry.Key); + } + } - /// - /// Returns the app instance identified by the given name. - /// - /// The instance with the specified name or null if it - /// doesn't exist. - /// If the name argument is null or empty. - /// Name of the app to retrieve. - public static FirebaseApp GetInstance(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException("App name to lookup must not be null or empty"); + this.services.Clear(); } - lock (Apps) + + // Clean up global state + lock (Apps) { - FirebaseApp app; - if (Apps.TryGetValue(name, out app)) - { - return app; - } + Apps.Remove(this.Name); } - return null; } /// @@ -302,11 +253,74 @@ internal static void DeleteAll() { entry.Value.Delete(); } + if (Apps.Count > 0) { throw new InvalidOperationException("Failed to delete all apps"); } - } + } + } + + internal T GetOrInit(string id, ServiceFactory initializer) + where T : class, IFirebaseService + { + lock (this.appLock) + { + if (this.deleted) + { + throw new InvalidOperationException("Cannot use an app after it has been deleted"); + } + + IFirebaseService service; + if (!this.services.TryGetValue(id, out service)) + { + service = initializer(); + this.services.Add(id, service); + } + + return (T)service; + } + } + + /// + /// Returns the Google Cloud Platform project ID associated with this Firebase app. If a + /// project ID is specified in , that value is returned. If not + /// attempts to determine a project ID from the used to + /// initialize the app. Looks up the GOOGLE_CLOUD_PROJECT environment variable when all + /// else fails. + /// + /// A project ID string or null. + internal string GetProjectId() + { + if (!string.IsNullOrEmpty(this.Options.ProjectId)) + { + return this.Options.ProjectId; + } + + var projectId = this.Options.Credential.ToServiceAccountCredential()?.ProjectId; + if (!string.IsNullOrEmpty(projectId)) + { + return projectId; + } + + foreach (var variableName in new[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" }) + { + projectId = Environment.GetEnvironmentVariable(variableName); + if (!string.IsNullOrEmpty(projectId)) + { + return projectId; + } + } + + return null; + } + + private static AppOptions GetOptionsFromEnvironment() + { + return new AppOptions() + { + Credential = GoogleCredential.GetApplicationDefault(), + }; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs index a2032d0a..e2a52733 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs @@ -19,10 +19,12 @@ namespace FirebaseAdmin /// /// Common error type for all exceptions raised by Firebase APIs. /// - public sealed class FirebaseException: Exception + public sealed class FirebaseException : Exception { - internal FirebaseException(string message): base(message) {} - - internal FirebaseException(string message, Exception inner): base(message, inner) {} + internal FirebaseException(string message) + : base(message) { } + + internal FirebaseException(string message, Exception inner) + : base(message, inner) { } } } diff --git a/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs b/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs index d6075f4a..3407ef9f 100644 --- a/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs +++ b/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs @@ -20,6 +20,9 @@ namespace FirebaseAdmin /// internal interface IFirebaseService { + /// + /// Cleans up any state associated with this service making it unsuitable for further use. + /// void Delete(); } } diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 00000000..56f05687 --- /dev/null +++ b/stylecop.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "documentInternalElements": false + } + } +} diff --git a/stylecop.ruleset b/stylecop.ruleset new file mode 100644 index 00000000..8a4c10a4 --- /dev/null +++ b/stylecop.ruleset @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset new file mode 100644 index 00000000..aaef9717 --- /dev/null +++ b/stylecop_test.ruleset @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +