Skip to content

Commit 41727f8

Browse files
authored
feat(auth): Support for creating tenant-scoped session cookies (#233)
* feat(auth): Support for creating tenant-scoped session cookies * fix: Using inheritdoc on the overridden method
1 parent aac8393 commit 41727f8

File tree

5 files changed

+123
-65
lines changed

5 files changed

+123
-65
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
using System.Net.Http;
2020
using System.Threading.Tasks;
2121
using FirebaseAdmin.Auth.Jwt;
22+
using FirebaseAdmin.Auth.Jwt.Tests;
23+
using FirebaseAdmin.Auth.Multitenancy;
2224
using FirebaseAdmin.Tests;
2325
using FirebaseAdmin.Util;
2426
using Google.Apis.Json;
@@ -1759,12 +1761,12 @@ public void RevokeRefreshTokensInvalidUid(TestConfig config)
17591761
async () => await auth.RevokeRefreshTokensAsync(uid));
17601762
}
17611763

1762-
[Fact]
1763-
public void CreateSessionCookieNoIdToken()
1764+
[Theory]
1765+
[MemberData(nameof(TestConfigs))]
1766+
public void CreateSessionCookieNoIdToken(TestConfig config)
17641767
{
1765-
var config = TestConfig.ForFirebaseAuth();
17661768
var handler = new MockMessageHandler() { Response = "{}" };
1767-
var auth = (FirebaseAuth)config.CreateAuth(handler);
1769+
var auth = config.CreateAuthWithIdTokenVerifier(handler);
17681770
var options = new SessionCookieOptions()
17691771
{
17701772
ExpiresIn = TimeSpan.FromHours(1),
@@ -1776,89 +1778,110 @@ public void CreateSessionCookieNoIdToken()
17761778
async () => await auth.CreateSessionCookieAsync(string.Empty, options));
17771779
}
17781780

1779-
[Fact]
1780-
public void CreateSessionCookieNoOptions()
1781+
[Theory]
1782+
[MemberData(nameof(TestConfigs))]
1783+
public void CreateSessionCookieNoOptions(TestConfig config)
17811784
{
1782-
var config = TestConfig.ForFirebaseAuth();
17831785
var handler = new MockMessageHandler() { Response = "{}" };
1784-
var auth = (FirebaseAuth)config.CreateAuth(handler);
1786+
var auth = config.CreateAuth(handler);
17851787

17861788
Assert.ThrowsAsync<ArgumentNullException>(
17871789
async () => await auth.CreateSessionCookieAsync("idToken", null));
17881790
}
17891791

1790-
[Fact]
1791-
public void CreateSessionCookieNoExpiresIn()
1792+
[Theory]
1793+
[MemberData(nameof(TestConfigs))]
1794+
public void CreateSessionCookieNoExpiresIn(TestConfig config)
17921795
{
1793-
var config = TestConfig.ForFirebaseAuth();
17941796
var handler = new MockMessageHandler() { Response = "{}" };
1795-
var auth = (FirebaseAuth)config.CreateAuth(handler);
1797+
var auth = config.CreateAuth(handler);
17961798

17971799
Assert.ThrowsAsync<ArgumentException>(
17981800
async () => await auth.CreateSessionCookieAsync(
17991801
"idToken", new SessionCookieOptions()));
18001802
}
18011803

1802-
[Fact]
1803-
public void CreateSessionCookieExpiresInTooLow()
1804+
[Theory]
1805+
[MemberData(nameof(TestConfigs))]
1806+
public async Task CreateSessionCookieExpiresInTooLow(TestConfig config)
18041807
{
1805-
var config = TestConfig.ForFirebaseAuth();
18061808
var handler = new MockMessageHandler() { Response = "{}" };
1807-
var auth = (FirebaseAuth)config.CreateAuth(handler);
1809+
var auth = config.CreateAuth(handler);
18081810
var fiveMinutesInSeconds = TimeSpan.FromMinutes(5).TotalSeconds;
18091811
var options = new SessionCookieOptions()
18101812
{
18111813
ExpiresIn = TimeSpan.FromSeconds(fiveMinutesInSeconds - 1),
18121814
};
18131815

1814-
Assert.ThrowsAsync<ArgumentException>(
1816+
await Assert.ThrowsAsync<ArgumentException>(
18151817
async () => await auth.CreateSessionCookieAsync("idToken", options));
18161818
}
18171819

1818-
[Fact]
1819-
public void CreateSessionCookieExpiresInTooHigh()
1820+
[Theory]
1821+
[MemberData(nameof(TestConfigs))]
1822+
public async Task CreateSessionCookieExpiresInTooHigh(TestConfig config)
18201823
{
1821-
var config = TestConfig.ForFirebaseAuth();
18221824
var handler = new MockMessageHandler() { Response = "{}" };
1823-
var auth = (FirebaseAuth)config.CreateAuth(handler);
1825+
var auth = config.CreateAuth(handler);
18241826
var fourteenDaysInSeconds = TimeSpan.FromDays(14).TotalSeconds;
18251827
var options = new SessionCookieOptions()
18261828
{
18271829
ExpiresIn = TimeSpan.FromSeconds(fourteenDaysInSeconds + 1),
18281830
};
18291831

1830-
Assert.ThrowsAsync<ArgumentException>(
1832+
await Assert.ThrowsAsync<ArgumentException>(
18311833
async () => await auth.CreateSessionCookieAsync("idToken", options));
18321834
}
18331835

1834-
[Fact]
1835-
public async Task CreateSessionCookie()
1836+
[Theory]
1837+
[MemberData(nameof(TestConfigs))]
1838+
public async Task CreateSessionCookie(TestConfig config)
18361839
{
1837-
var config = TestConfig.ForFirebaseAuth();
18381840
var handler = new MockMessageHandler()
18391841
{
18401842
Response = @"{
18411843
""sessionCookie"": ""cookie""
18421844
}",
18431845
};
1844-
var auth = (FirebaseAuth)config.CreateAuth(handler);
1846+
var auth = config.CreateAuthWithIdTokenVerifier(handler);
1847+
var idToken = await CreateIdTokenAsync(config.TenantId);
18451848
var options = new SessionCookieOptions()
18461849
{
18471850
ExpiresIn = TimeSpan.FromHours(1),
18481851
};
18491852

1850-
var result = await auth.CreateSessionCookieAsync("idToken", options);
1853+
var result = await auth.CreateSessionCookieAsync(idToken, options);
18511854

18521855
Assert.Equal("cookie", result);
18531856
Assert.Equal(1, handler.Requests.Count);
18541857
var request = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(handler.LastRequestBody);
18551858
Assert.Equal(2, request.Count);
1856-
Assert.Equal("idToken", request["idToken"]);
1859+
Assert.Equal(idToken, request["idToken"]);
18571860
Assert.Equal(3600, request["validDuration"]);
18581861

18591862
config.AssertRequest(":createSessionCookie", Assert.Single(handler.Requests));
18601863
}
18611864

1865+
[Fact]
1866+
public async Task CreateSessionCookieTenantIdMismatch()
1867+
{
1868+
var config = TestConfig.ForTenantAwareFirebaseAuth("test-tenant");
1869+
var auth = (TenantAwareFirebaseAuth)config.CreateAuthWithIdTokenVerifier();
1870+
var idToken = await CreateIdTokenAsync("other-tenant");
1871+
var options = new SessionCookieOptions()
1872+
{
1873+
ExpiresIn = TimeSpan.FromHours(1),
1874+
};
1875+
1876+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
1877+
() => auth.CreateSessionCookieAsync(idToken, options));
1878+
1879+
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
1880+
Assert.Equal(AuthErrorCode.TenantIdMismatch, exception.AuthErrorCode);
1881+
Assert.Null(exception.InnerException);
1882+
Assert.Null(exception.HttpResponse);
1883+
}
1884+
18621885
[Theory]
18631886
[MemberData(nameof(TestConfigs))]
18641887
public async Task ServiceUnvailable(TestConfig config)
@@ -1929,27 +1952,35 @@ private static FirebaseUserManager.Args CreateArgs()
19291952
};
19301953
}
19311954

1955+
private static async Task<string> CreateIdTokenAsync(string tenantId)
1956+
{
1957+
var tokenBuilder = JwtTestUtils.IdTokenBuilder(tenantId);
1958+
tokenBuilder.ProjectId = TestConfig.MockProjectId;
1959+
return await tokenBuilder.CreateTokenAsync();
1960+
}
1961+
19321962
public class TestConfig
19331963
{
19341964
internal const string MockProjectId = "project1";
19351965

19361966
internal static readonly IClock Clock = new MockClock();
19371967

1938-
private readonly string tenantId;
19391968
private readonly AuthBuilder authBuilder;
19401969

19411970
private TestConfig(string tenantId = null)
19421971
{
1943-
this.tenantId = tenantId;
19441972
this.authBuilder = new AuthBuilder
19451973
{
19461974
ProjectId = MockProjectId,
19471975
Clock = Clock,
19481976
RetryOptions = RetryOptions.NoBackOff,
1977+
KeySource = JwtTestUtils.DefaultKeySource,
19491978
TenantId = tenantId,
19501979
};
19511980
}
19521981

1982+
public string TenantId => this.authBuilder.TenantId;
1983+
19531984
public static TestConfig ForFirebaseAuth()
19541985
{
19551986
return new TestConfig();
@@ -1969,10 +2000,21 @@ public AbstractFirebaseAuth CreateAuth(HttpMessageHandler handler = null)
19692000
return this.authBuilder.Build(options);
19702001
}
19712002

2003+
public AbstractFirebaseAuth CreateAuthWithIdTokenVerifier(
2004+
HttpMessageHandler handler = null)
2005+
{
2006+
var options = new TestOptions
2007+
{
2008+
UserManagerRequestHandler = handler,
2009+
IdTokenVerifier = true,
2010+
};
2011+
return this.authBuilder.Build(options);
2012+
}
2013+
19722014
internal void AssertRequest(
19732015
string expectedSuffix, MockMessageHandler.IncomingRequest request)
19742016
{
1975-
var tenantInfo = this.tenantId != null ? $"/tenants/{this.tenantId}" : string.Empty;
2017+
var tenantInfo = this.TenantId != null ? $"/tenants/{this.TenantId}" : string.Empty;
19762018
var expectedPath = $"/v1/projects/{MockProjectId}{tenantInfo}/{expectedSuffix}";
19772019
Assert.Equal(expectedPath, request.Url.PathAndQuery);
19782020
}

FirebaseAdmin/FirebaseAdmin/Auth/AbstractFirebaseAuth.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,39 @@ public async Task<string> GenerateSignInWithEmailLinkAsync(
978978
.ConfigureAwait(false);
979979
}
980980

981+
/// <summary>
982+
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
983+
/// be set as a server-side session cookie with a custom cookie policy.
984+
/// </summary>
985+
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
986+
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
987+
/// <param name="options">Additional options required to create the cookie.</param>
988+
/// <returns>A task that completes with the Firebase session cookie.</returns>
989+
public async Task<string> CreateSessionCookieAsync(
990+
string idToken, SessionCookieOptions options)
991+
{
992+
return await this.CreateSessionCookieAsync(idToken, options, default(CancellationToken))
993+
.ConfigureAwait(false);
994+
}
995+
996+
/// <summary>
997+
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
998+
/// be set as a server-side session cookie with a custom cookie policy.
999+
/// </summary>
1000+
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
1001+
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
1002+
/// <param name="options">Additional options required to create the cookie.</param>
1003+
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
1004+
/// operation.</param>
1005+
/// <returns>A task that completes with the Firebase session cookie.</returns>
1006+
public virtual async Task<string> CreateSessionCookieAsync(
1007+
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
1008+
{
1009+
return await this.UserManager
1010+
.CreateSessionCookieAsync(idToken, options, cancellationToken)
1011+
.ConfigureAwait(false);
1012+
}
1013+
9811014
/// <summary>
9821015
/// Parses and verifies a Firebase session cookie.
9831016
///

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -83,39 +83,6 @@ public static FirebaseAuth GetAuth(FirebaseApp app)
8383
});
8484
}
8585

86-
/// <summary>
87-
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
88-
/// be set as a server-side session cookie with a custom cookie policy.
89-
/// </summary>
90-
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
91-
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
92-
/// <param name="options">Additional options required to create the cookie.</param>
93-
/// <returns>A task that completes with the Firebase session cookie.</returns>
94-
public async Task<string> CreateSessionCookieAsync(
95-
string idToken, SessionCookieOptions options)
96-
{
97-
return await this.CreateSessionCookieAsync(idToken, options, default(CancellationToken))
98-
.ConfigureAwait(false);
99-
}
100-
101-
/// <summary>
102-
/// Creates a new Firebase session cookie from the given ID token and options. The returned JWT can
103-
/// be set as a server-side session cookie with a custom cookie policy.
104-
/// </summary>
105-
/// <exception cref="FirebaseAuthException">If an error occurs while creating the cookie.</exception>
106-
/// <param name="idToken">The Firebase ID token to exchange for a session cookie.</param>
107-
/// <param name="options">Additional options required to create the cookie.</param>
108-
/// <param name="cancellationToken">A cancellation token to monitor the asynchronous
109-
/// operation.</param>
110-
/// <returns>A task that completes with the Firebase session cookie.</returns>
111-
public async Task<string> CreateSessionCookieAsync(
112-
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
113-
{
114-
return await this.UserManager
115-
.CreateSessionCookieAsync(idToken, options, cancellationToken)
116-
.ConfigureAwait(false);
117-
}
118-
11986
/// <summary>
12087
/// Looks up an OIDC auth provider configuration by the provided ID.
12188
/// </summary>

FirebaseAdmin/FirebaseAdmin/Auth/Multitenancy/TenantAwareFirebaseAuth.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Threading;
17+
using System.Threading.Tasks;
1618
using FirebaseAdmin.Auth.Jwt;
19+
using Google.Apis.Util;
1720

1821
namespace FirebaseAdmin.Auth.Multitenancy
1922
{
@@ -38,6 +41,17 @@ internal TenantAwareFirebaseAuth(Args args)
3841
/// </summary>
3942
public string TenantId { get; }
4043

44+
/// <inheritdoc/>
45+
public override async Task<string> CreateSessionCookieAsync(
46+
string idToken, SessionCookieOptions options, CancellationToken cancellationToken)
47+
{
48+
// As a minor optimization, validate options here before calling VerifyIdToken().
49+
options.ThrowIfNull(nameof(options)).CopyAndValidate();
50+
await this.VerifyIdTokenAsync(idToken, cancellationToken);
51+
return await base.CreateSessionCookieAsync(idToken, options, cancellationToken)
52+
.ConfigureAwait(false);
53+
}
54+
4155
internal static TenantAwareFirebaseAuth Create(FirebaseApp app, string tenantId)
4256
{
4357
var args = new Args

FirebaseAdmin/FirebaseAdmin/Auth/SessionCookieOptions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
namespace FirebaseAdmin.Auth
1818
{
1919
/// <summary>
20-
/// Options for the <see cref="FirebaseAuth.CreateSessionCookieAsync(string, SessionCookieOptions)"/> API.
20+
/// Options for the
21+
/// <see cref="AbstractFirebaseAuth.CreateSessionCookieAsync(string, SessionCookieOptions)"/>
22+
/// API.
2123
/// </summary>
2224
public sealed class SessionCookieOptions
2325
{

0 commit comments

Comments
 (0)