Skip to content

Commit 9d5976f

Browse files
authored
feat(auth): Add support for Firebase Authentication Emulator (#289)
* Add IdToolkitHostResolver with tests * Add resolution for versions of the API with tests * Simplify and reduce IdToolkitHostResolver to a Util method * Add TestConfig for testing against FirebaseEmulatorHost Add environment variable test that piggybacks on the already established FirebaseUserManagerTest. Also, to make it easier to access and set environment variables a convenience class has been made to help out. This class utilizes the neat builtin get and set mechanism provided by the C# language. By using this class the code somewhat documents itself and prevents stupid spelling mistakes that might occur when running code. Each environment variable can conveniently be documented inside the class. * Add license for new Utils class file * Add documentation for ResolveIdToolkitHost with examples * Use the EnvironmentVariable class to access env variables * Fix formatting * Use inheritance instead of TestConfig to set FirebaseUserManager tests The TestConfig was not being disposed of when expected resulting in the env variable being set in other tests as well and causing them to fail. * Cleanup and rename variable to more meaningful name * Move Utils and EnvironmentVariable class to Auth namespace * Move emulatorHostEnvVar to where it's used for readability * Rename UtilTest to more appropriate name * Rename expected...Host to expected...Url * Use the FirebaseAuthEmulatorHostName const instead of string * Add missing license head * Add missing util import and fix stylecop complaints * Rename AuthUtil to Util and move to Auth test folder * Simplify emulator host environment variable to be a constant in Utils * Use string literal for emulator host in FirebaseUserManagerTest In case a constant values is changed such as the used environment variable, this will cause the tests to fail, which is good to know. * Remove unnecessary tests from FirebaseAuthTest * Use string literal to ensure test fails if constant is changed * Remove the environment simplification since there is only one callsite * Cleanup GetIdToolkitHost documentation * Correct typo in file name. Util -> Utils. * Rename test cass to UtilsTest * Add tenant resolution to GetIdToolkitHost * Add function for resolving GoogleCredentials when in emulator mode * Add test for tenant url resolution * Use if statement instead of ternary for tenantIdPath * Better formatting for GetIdToolkitHost * Add periods to the end of documentation strings * Remove nullable and provide empty string instead * Change to only have one single line at end * Add convenience funcs for getting and checking emulator host env variable * Simplify GetEmulatorHost name
1 parent f6babbd commit 9d5976f

File tree

6 files changed

+190
-27
lines changed

6 files changed

+190
-27
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using FirebaseAdmin.Auth.Jwt;
1717
using Google.Apis.Auth.OAuth2;
1818
using Xunit;
19+
using static FirebaseAdmin.Auth.Utils;
1920

2021
[assembly: CollectionBehavior(DisableTestParallelization = true)]
2122
namespace FirebaseAdmin.Auth.Tests
@@ -71,10 +72,10 @@ public void UseAfterDelete()
7172
public void NoTenantId()
7273
{
7374
var app = FirebaseApp.Create(new AppOptions
74-
{
75-
Credential = MockCredential,
76-
ProjectId = "project1",
77-
});
75+
{
76+
Credential = MockCredential,
77+
ProjectId = "project1",
78+
});
7879

7980
FirebaseAuth auth = FirebaseAuth.DefaultInstance;
8081

@@ -127,17 +128,17 @@ public void TenantManagerNoProjectId()
127128
public void ServiceAccountId()
128129
{
129130
FirebaseApp.Create(new AppOptions
130-
{
131-
Credential = MockCredential,
132-
ServiceAccountId = "test-service-account",
133-
});
131+
{
132+
Credential = MockCredential,
133+
ServiceAccountId = "test-service-account",
134+
});
134135

135136
var tokenFactory = FirebaseAuth.DefaultInstance.TokenFactory;
136137

137138
Assert.IsType<FixedAccountIAMSigner>(tokenFactory.Signer);
138139
}
139140

140-
public void Dispose()
141+
public virtual void Dispose()
141142
{
142143
FirebaseApp.DeleteAll();
143144
}

FirebaseAdmin/FirebaseAdmin.Tests/Auth/Users/FirebaseUserManagerTest.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
using Google.Apis.Util;
3030
using Newtonsoft.Json.Linq;
3131
using Xunit;
32+
using static FirebaseAdmin.Auth.Utils;
3233

3334
namespace FirebaseAdmin.Auth.Users.Tests
3435
{
@@ -2108,7 +2109,6 @@ private static async Task<string> CreateIdTokenAsync(string tenantId)
21082109
public class TestConfig
21092110
{
21102111
internal const string MockProjectId = "project1";
2111-
21122112
internal static readonly IClock Clock = new MockClock();
21132113

21142114
private readonly AuthBuilder authBuilder;
@@ -2195,8 +2195,8 @@ internal void AssertRequest(
21952195
string expectedSuffix, MockMessageHandler.IncomingRequest request)
21962196
{
21972197
var tenantInfo = this.TenantId != null ? $"/tenants/{this.TenantId}" : string.Empty;
2198-
var expectedPath = $"/v1/projects/{MockProjectId}{tenantInfo}/{expectedSuffix}";
2199-
Assert.Equal(expectedPath, request.Url.PathAndQuery);
2198+
var expectedUrl = $"{Utils.GetIdToolkitHost(MockProjectId, IdToolkitVersion.V1)}{tenantInfo}/{expectedSuffix}";
2199+
Assert.Equal(expectedUrl, request.Url.ToString());
22002200
}
22012201

22022202
private IDictionary<string, object> GetUserResponseDictionary(string response = null)
@@ -2224,4 +2224,16 @@ private IDictionary<string, object> GetUserResponseDictionary(string response =
22242224
}
22252225
}
22262226
}
2227+
2228+
public class EmulatorFirebaseUserManagerTest : FirebaseUserManagerTest, IDisposable
2229+
{
2230+
public EmulatorFirebaseUserManagerTest()
2231+
=> this.SetFirebaseHostEnvironmentVariable("localhost:9099");
2232+
2233+
public void Dispose()
2234+
=> this.SetFirebaseHostEnvironmentVariable(string.Empty);
2235+
2236+
private void SetFirebaseHostEnvironmentVariable(string value)
2237+
=> Environment.SetEnvironmentVariable("FIREBASE_AUTH_EMULATOR_HOST", value);
2238+
}
22272239
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2021, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using Xunit;
17+
using static FirebaseAdmin.Auth.Utils;
18+
19+
namespace FirebaseAdmin.Auth.Tests
20+
{
21+
public class UtilsTest : IDisposable
22+
{
23+
private const string MockProjectId = "test_project1234";
24+
private const string CustomHost = "localhost:9099";
25+
26+
[Fact]
27+
public void ResolvesToCorrectVersion()
28+
{
29+
var expectedV1Url = $"https://identitytoolkit.googleapis.com/v1/projects/{MockProjectId}";
30+
var expectedV2Url = $"https://identitytoolkit.googleapis.com/v2/projects/{MockProjectId}";
31+
32+
Assert.Equal(expectedV1Url, Utils.GetIdToolkitHost(MockProjectId, IdToolkitVersion.V1));
33+
Assert.Equal(expectedV2Url, Utils.GetIdToolkitHost(MockProjectId, IdToolkitVersion.V2));
34+
}
35+
36+
[Fact]
37+
public void ResolvesToEmulatorHost()
38+
{
39+
Environment.SetEnvironmentVariable("FIREBASE_AUTH_EMULATOR_HOST", CustomHost);
40+
41+
var expectedUrl = $"http://{CustomHost}/identitytoolkit.googleapis.com/v2/projects/{MockProjectId}";
42+
43+
Assert.Equal(expectedUrl, Utils.GetIdToolkitHost(MockProjectId, IdToolkitVersion.V2));
44+
}
45+
46+
[Fact]
47+
public void ResolvesToEmulatorHostWithTenantId()
48+
{
49+
Environment.SetEnvironmentVariable("FIREBASE_AUTH_EMULATOR_HOST", CustomHost);
50+
51+
var tenantId = "test-tenant1";
52+
var expectedUrl = $"http://{CustomHost}/identitytoolkit.googleapis.com/v2/projects/{MockProjectId}/tenants/{tenantId}";
53+
54+
Assert.Equal(expectedUrl, Utils.GetIdToolkitHost(MockProjectId, IdToolkitVersion.V2, tenantId));
55+
}
56+
57+
[Fact]
58+
public void FailsOnNoProjectId()
59+
{
60+
Assert.Throws<ArgumentException>(() => Utils.GetIdToolkitHost(string.Empty, IdToolkitVersion.V2));
61+
}
62+
63+
[Fact]
64+
public void ResolvesToFirebaseHost()
65+
{
66+
var expectedUrl = $"https://identitytoolkit.googleapis.com/v2/projects/{MockProjectId}";
67+
Assert.Equal(expectedUrl, Utils.GetIdToolkitHost(MockProjectId, IdToolkitVersion.V2));
68+
}
69+
70+
public void Dispose()
71+
{
72+
Environment.SetEnvironmentVariable("FIREBASE_AUTH_EMULATOR_HOST", string.Empty);
73+
}
74+
}
75+
}

FirebaseAdmin/FirebaseAdmin/Auth/Multitenancy/TenantManager.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ namespace FirebaseAdmin.Auth.Multitenancy
4545
/// </summary>
4646
public sealed class TenantManager : IDisposable
4747
{
48-
private const string IdToolkitUrl = "https://identitytoolkit.googleapis.com/v2/projects/{0}";
49-
5048
private const string ClientVersionHeader = "X-Client-Version";
5149

5250
private static readonly string ClientVersion = $"DotNet/Admin/{FirebaseApp.GetSdkVersion()}";
@@ -73,7 +71,8 @@ internal TenantManager(Args args)
7371
}
7472

7573
this.app = args.App;
76-
this.baseUrl = string.Format(IdToolkitUrl, args.ProjectId);
74+
75+
this.baseUrl = Utils.GetIdToolkitHost(args.ProjectId, IdToolkitVersion.V2);
7776
this.httpClient = new ErrorHandlingHttpClient<FirebaseAuthException>(
7877
args.ToHttpClientArgs());
7978
}

FirebaseAdmin/FirebaseAdmin/Auth/Users/FirebaseUserManager.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ internal class FirebaseUserManager : IDisposable
4343

4444
internal static readonly string ClientVersion = $"DotNet/Admin/{FirebaseApp.GetSdkVersion()}";
4545

46-
private const string IdToolkitUrl = "https://identitytoolkit.googleapis.com/v1/projects/{0}";
47-
4846
/** Maximum allowed number of users to batch get at one time. */
4947
private const int MaxGetAccountsBatchSize = 100;
5048

@@ -73,22 +71,14 @@ internal FirebaseUserManager(Args args)
7371
new ErrorHandlingHttpClientArgs<FirebaseAuthException>()
7472
{
7573
HttpClientFactory = args.ClientFactory,
76-
Credential = args.Credential,
74+
Credential = Utils.ResolveCredentials(args.Credential),
7775
ErrorResponseHandler = AuthErrorHandler.Instance,
7876
RequestExceptionHandler = AuthErrorHandler.Instance,
7977
DeserializeExceptionHandler = AuthErrorHandler.Instance,
8078
RetryOptions = args.RetryOptions,
8179
});
8280
this.clock = args.Clock ?? SystemClock.Default;
83-
var baseUrl = string.Format(IdToolkitUrl, args.ProjectId);
84-
if (this.TenantId != null)
85-
{
86-
this.baseUrl = $"{baseUrl}/tenants/{this.TenantId}";
87-
}
88-
else
89-
{
90-
this.baseUrl = baseUrl;
91-
}
81+
this.baseUrl = Utils.GetIdToolkitHost(args.ProjectId, IdToolkitVersion.V1, this.TenantId);
9282
}
9383

9484
internal string TenantId { get; }
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2021, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
using System;
15+
using Google.Apis.Auth.OAuth2;
16+
17+
namespace FirebaseAdmin.Auth
18+
{
19+
internal enum IdToolkitVersion
20+
{
21+
V1,
22+
V2,
23+
}
24+
25+
internal class Utils
26+
{
27+
/// <summary>
28+
/// <para>Gets the correct identity toolkit api host.</para>
29+
/// <para>It does this by checking if FIREBASE_AUTH_EMULATOR_HOST exists
30+
/// and then prepends the url with that host if it does. Otherwise it returns the regular identity host.</para>
31+
/// <para>Example:<br/>
32+
/// If FIREBASE_AUTH_EMULATOR_HOST environment variable is set to localhost:9099 the host is resolved to http://localhost:9099/identitytoolkit.googleapis.com&#x2026;.<br/>
33+
/// If FIREBASE_AUTH_EMULATOR_HOST environment variable is not set the host resolves to https://identitytoolkit.googleapis.com&#x2026;.</para>
34+
/// </summary>
35+
/// <param name="projectId">The project ID to connect to.</param>
36+
/// <param name="version">The version of the API to connect to.</param>
37+
/// <param name="tenantId">The tenant id.</param>
38+
/// <returns>Resolved identity toolkit host.</returns>
39+
internal static string GetIdToolkitHost(string projectId, IdToolkitVersion version = IdToolkitVersion.V2, string tenantId = "")
40+
{
41+
if (string.IsNullOrWhiteSpace(projectId))
42+
{
43+
throw new ArgumentException("Must provide a project ID to resolve");
44+
}
45+
46+
const string IdToolkitUrl = "https://identitytoolkit.googleapis.com/{0}/projects/{1}{2}";
47+
const string IdToolkitEmulatorUrl = "http://{0}/identitytoolkit.googleapis.com/{1}/projects/{2}{3}";
48+
49+
var tenantIdPath = string.Empty;
50+
if (!string.IsNullOrWhiteSpace(tenantId))
51+
{
52+
tenantIdPath = $"/tenants/{tenantId}";
53+
}
54+
55+
var versionAsString = version.ToString().ToLower();
56+
57+
if (IsEmulatorHostSet())
58+
{
59+
return string.Format(
60+
IdToolkitEmulatorUrl,
61+
GetEmulatorHost(),
62+
versionAsString,
63+
projectId,
64+
tenantIdPath);
65+
}
66+
67+
return string.Format(IdToolkitUrl, versionAsString, projectId, tenantIdPath);
68+
}
69+
70+
internal static GoogleCredential ResolveCredentials(GoogleCredential original)
71+
{
72+
if (IsEmulatorHostSet())
73+
{
74+
return GoogleCredential.FromAccessToken("owner");
75+
}
76+
77+
return original;
78+
}
79+
80+
private static bool IsEmulatorHostSet()
81+
=> !string.IsNullOrWhiteSpace(GetEmulatorHost());
82+
83+
private static string GetEmulatorHost()
84+
=> Environment.GetEnvironmentVariable("FIREBASE_AUTH_EMULATOR_HOST");
85+
}
86+
}

0 commit comments

Comments
 (0)