Skip to content

Commit aac8393

Browse files
authored
feat(auth): Support for verifying tenant-scoped session cookies (#232)
* feat(auth): Implemented support for verifying tenant-scoped session cookies * fix: Removing unused test util method
1 parent cddaeb5 commit aac8393

13 files changed

+464
-386
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/AuthBuilder.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ private void PopulateArgs(AbstractFirebaseAuth.Args args, TestOptions options)
6666
args.IdTokenVerifier = new Lazy<FirebaseTokenVerifier>(
6767
this.CreateIdTokenVerifier());
6868
}
69+
70+
if (options.SessionCookieVerifier)
71+
{
72+
args.SessionCookieVerifier = new Lazy<FirebaseTokenVerifier>(
73+
this.CreateSessionCookieVerifier());
74+
}
6975
}
7076

7177
private FirebaseUserManager CreateUserManager(TestOptions options)
@@ -86,5 +92,11 @@ private FirebaseTokenVerifier CreateIdTokenVerifier()
8692
return FirebaseTokenVerifier.CreateIdTokenVerifier(
8793
this.ProjectId, this.KeySource, this.Clock, this.TenantId);
8894
}
95+
96+
private FirebaseTokenVerifier CreateSessionCookieVerifier()
97+
{
98+
return FirebaseTokenVerifier.CreateSessionCookieVerifier(
99+
this.ProjectId, this.KeySource, this.Clock, this.TenantId);
100+
}
89101
}
90102
}

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,9 @@ public async Task UseAfterDelete()
6262
app.Delete();
6363

6464
Assert.Throws<InvalidOperationException>(() => auth.TokenFactory);
65-
await Assert.ThrowsAsync<InvalidOperationException>(
66-
async () => await auth.VerifyIdTokenAsync("user"));
67-
await Assert.ThrowsAsync<InvalidOperationException>(
68-
async () => await auth.SetCustomUserClaimsAsync("user", null));
65+
Assert.Throws<InvalidOperationException>(() => auth.IdTokenVerifier);
66+
Assert.Throws<InvalidOperationException>(() => auth.SessionCookieVerifier);
67+
Assert.Throws<InvalidOperationException>(() => auth.UserManager);
6968
await Assert.ThrowsAsync<InvalidOperationException>(
7069
async () => await auth.GetOidcProviderConfigAsync("oidc.provider"));
7170
Assert.Throws<InvalidOperationException>(() => auth.TenantManager);
@@ -84,6 +83,7 @@ public void NoTenantId()
8483

8584
Assert.Null(auth.TokenFactory.TenantId);
8685
Assert.Null(auth.IdTokenVerifier.TenantId);
86+
Assert.Null(auth.SessionCookieVerifier.TenantId);
8787
Assert.Null(auth.UserManager.TenantId);
8888
}
8989

FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/IdTokenVerificationTest.cs

Lines changed: 30 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
using System.Threading.Tasks;
2121
using FirebaseAdmin.Auth.Tests;
2222
using FirebaseAdmin.Tests;
23-
using FirebaseAdmin.Util;
24-
using Google.Apis.Util;
2523
using Xunit;
2624

2725
namespace FirebaseAdmin.Auth.Jwt.Tests
@@ -36,9 +34,6 @@ public class IdTokenVerificationTest
3634

3735
private const long ClockSkewSeconds = 5 * 60;
3836

39-
private static readonly TestOptions WithIdTokenVerifier =
40-
new TestOptions { IdTokenVerifier = true };
41-
4237
[Theory]
4338
[MemberData(nameof(TestConfigs))]
4439
public async Task ValidToken(TestConfig config)
@@ -148,7 +143,7 @@ public async Task IncorrectAlgorithm(TestConfig config)
148143
[MemberData(nameof(TestConfigs))]
149144
public async Task Expired(TestConfig config)
150145
{
151-
var expiryTime = TestConfig.Clock.UnixTimestamp() - (ClockSkewSeconds + 1);
146+
var expiryTime = JwtTestUtils.Clock.UnixTimestamp() - (ClockSkewSeconds + 1);
152147
var payload = new Dictionary<string, object>()
153148
{
154149
{ "exp", expiryTime },
@@ -160,15 +155,15 @@ public async Task Expired(TestConfig config)
160155
async () => await auth.VerifyIdTokenAsync(idToken));
161156

162157
var expectedMessage = $"Firebase ID token expired at {expiryTime}. "
163-
+ $"Expected to be greater than {TestConfig.Clock.UnixTimestamp()}.";
158+
+ $"Expected to be greater than {JwtTestUtils.Clock.UnixTimestamp()}.";
164159
this.CheckException(exception, expectedMessage, AuthErrorCode.ExpiredIdToken);
165160
}
166161

167162
[Theory]
168163
[MemberData(nameof(TestConfigs))]
169164
public async Task ExpiryTimeInAcceptableRange(TestConfig config)
170165
{
171-
var expiryTimeSeconds = TestConfig.Clock.UnixTimestamp() - ClockSkewSeconds;
166+
var expiryTimeSeconds = JwtTestUtils.Clock.UnixTimestamp() - ClockSkewSeconds;
172167
var payload = new Dictionary<string, object>()
173168
{
174169
{ "exp", expiryTimeSeconds },
@@ -178,15 +173,14 @@ public async Task ExpiryTimeInAcceptableRange(TestConfig config)
178173

179174
var decoded = await auth.VerifyIdTokenAsync(idToken);
180175

181-
Assert.Equal("testuser", decoded.Uid);
182-
Assert.Equal(expiryTimeSeconds, decoded.ExpirationTimeSeconds);
176+
config.AssertFirebaseToken(decoded, payload);
183177
}
184178

185179
[Theory]
186180
[MemberData(nameof(TestConfigs))]
187181
public async Task InvalidIssuedAt(TestConfig config)
188182
{
189-
var issuedAt = TestConfig.Clock.UnixTimestamp() + (ClockSkewSeconds + 1);
183+
var issuedAt = JwtTestUtils.Clock.UnixTimestamp() + (ClockSkewSeconds + 1);
190184
var payload = new Dictionary<string, object>()
191185
{
192186
{ "iat", issuedAt },
@@ -205,7 +199,7 @@ public async Task InvalidIssuedAt(TestConfig config)
205199
[MemberData(nameof(TestConfigs))]
206200
public async Task IssuedAtInAcceptableRange(TestConfig config)
207201
{
208-
var issuedAtSeconds = TestConfig.Clock.UnixTimestamp() + ClockSkewSeconds;
202+
var issuedAtSeconds = JwtTestUtils.Clock.UnixTimestamp() + ClockSkewSeconds;
209203
var payload = new Dictionary<string, object>()
210204
{
211205
{ "iat", issuedAtSeconds },
@@ -215,8 +209,7 @@ public async Task IssuedAtInAcceptableRange(TestConfig config)
215209

216210
var decoded = await auth.VerifyIdTokenAsync(idToken);
217211

218-
Assert.Equal("testuser", decoded.Uid);
219-
Assert.Equal(issuedAtSeconds, decoded.IssuedAtTimeSeconds);
212+
config.AssertFirebaseToken(decoded, payload);
220213
}
221214

222215
[Theory]
@@ -260,6 +253,21 @@ public async Task CustomToken(TestConfig config)
260253
this.CheckException(exception, expectedMessage);
261254
}
262255

256+
[Theory]
257+
[MemberData(nameof(TestConfigs))]
258+
public async Task SessionCookie(TestConfig config)
259+
{
260+
var tokenBuilder = JwtTestUtils.SessionCookieBuilder(config.TenantId);
261+
var sessionCookie = await tokenBuilder.CreateTokenAsync();
262+
var auth = config.CreateAuth();
263+
264+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
265+
async () => await auth.VerifyIdTokenAsync(sessionCookie));
266+
267+
var expectedMessage = "Firebase ID token has incorrect issuer (iss) claim.";
268+
this.CheckException(exception, expectedMessage);
269+
}
270+
263271
[Theory]
264272
[MemberData(nameof(TestConfigs))]
265273
public async Task InvalidAudience(TestConfig config)
@@ -327,7 +335,7 @@ public async Task RevokedToken(TestConfig config)
327335
""users"": [
328336
{{
329337
""localId"": ""testuser"",
330-
""validSince"": {TestConfig.Clock.UnixTimestamp()}
338+
""validSince"": {JwtTestUtils.Clock.UnixTimestamp()}
331339
}}
332340
]
333341
}}",
@@ -345,7 +353,7 @@ public async Task RevokedToken(TestConfig config)
345353
var expectedMessage = "Firebase ID token has been revoked.";
346354
this.CheckException(exception, expectedMessage, AuthErrorCode.RevokedIdToken);
347355
Assert.Equal(1, handler.Calls);
348-
config.AssertRequest("accounts:lookup", handler.Requests[0]);
356+
JwtTestUtils.AssertRevocationCheckRequest(config.TenantId, handler.Requests[0].Url);
349357
}
350358

351359
[Theory]
@@ -369,7 +377,7 @@ public async Task ValidUnrevokedToken(TestConfig config)
369377

370378
Assert.Equal("testuser", decoded.Uid);
371379
Assert.Equal(1, handler.Calls);
372-
config.AssertRequest("accounts:lookup", handler.Requests[0]);
380+
JwtTestUtils.AssertRevocationCheckRequest(config.TenantId, handler.Requests[0].Url);
373381
}
374382

375383
[Theory]
@@ -395,7 +403,7 @@ public async Task CheckRevokedError(TestConfig config)
395403
Assert.Null(exception.InnerException);
396404
Assert.NotNull(exception.HttpResponse);
397405
Assert.Equal(1, handler.Calls);
398-
config.AssertRequest("accounts:lookup", handler.Requests[0]);
406+
JwtTestUtils.AssertRevocationCheckRequest(config.TenantId, handler.Requests[0].Url);
399407
}
400408

401409
[Theory]
@@ -434,30 +442,6 @@ public async Task TenantIdMismatch(TestConfig config)
434442
this.CheckException(exception, expectedMessage, AuthErrorCode.TenantIdMismatch);
435443
}
436444

437-
// TODO(hkj): Remove the following method once the session cookie tests have been
438-
// refactored.
439-
440-
/// <summary>
441-
/// Creates a mock ID token for testing purposes. By default the created token has an issue
442-
/// time 10 minutes ago, and an expirty time 50 minutes into the future. All header and
443-
/// payload claims can be overridden if needed.
444-
/// </summary>
445-
internal static async Task<string> CreateTestTokenAsync(
446-
Dictionary<string, object> headerOverrides = null,
447-
Dictionary<string, object> payloadOverrides = null)
448-
{
449-
var tokenBuilder = new MockTokenBuilder
450-
{
451-
ProjectId = TestConfig.ProjectId,
452-
Clock = TestConfig.Clock,
453-
Signer = JwtTestUtils.DefaultSigner,
454-
IssuerPrefix = "https://securetoken.google.com",
455-
Uid = "testuser",
456-
};
457-
return await tokenBuilder.CreateTokenAsync(
458-
headerOverrides, payloadOverrides);
459-
}
460-
461445
private void CheckException(
462446
FirebaseAuthException exception,
463447
string prefix,
@@ -472,36 +456,17 @@ private void CheckException(
472456

473457
public class TestConfig
474458
{
475-
internal const string ProjectId = "test-project";
476-
477-
internal static readonly IClock Clock = new MockClock();
478-
479-
private readonly string tenantId;
480459
private readonly AuthBuilder authBuilder;
481460
private readonly MockTokenBuilder tokenBuilder;
482461

483462
private TestConfig(string tenantId = null)
484463
{
485-
this.tenantId = tenantId;
486-
this.authBuilder = new AuthBuilder
487-
{
488-
ProjectId = ProjectId,
489-
Clock = Clock,
490-
KeySource = JwtTestUtils.DefaultKeySource,
491-
RetryOptions = RetryOptions.NoBackOff,
492-
TenantId = tenantId,
493-
};
494-
this.tokenBuilder = new MockTokenBuilder
495-
{
496-
ProjectId = ProjectId,
497-
Clock = Clock,
498-
Signer = JwtTestUtils.DefaultSigner,
499-
IssuerPrefix = "https://securetoken.google.com",
500-
Uid = "testuser",
501-
TenantId = this.tenantId,
502-
};
464+
this.tokenBuilder = JwtTestUtils.IdTokenBuilder(tenantId);
465+
this.authBuilder = JwtTestUtils.AuthBuilderForTokenVerification(tenantId);
503466
}
504467

468+
public string TenantId => this.authBuilder.TenantId;
469+
505470
public static TestConfig ForFirebaseAuth()
506471
{
507472
return new TestConfig();
@@ -534,14 +499,6 @@ public void AssertFirebaseToken(
534499
{
535500
this.tokenBuilder.AssertFirebaseToken(token, expectedClaims);
536501
}
537-
538-
internal void AssertRequest(
539-
string expectedSuffix, MockMessageHandler.IncomingRequest request)
540-
{
541-
var tenantInfo = this.tenantId != null ? $"/tenants/{this.tenantId}" : string.Empty;
542-
var expectedPath = $"/v1/projects/{ProjectId}{tenantInfo}/{expectedSuffix}";
543-
Assert.Equal(expectedPath, request.Url.PathAndQuery);
544-
}
545502
}
546503
}
547504
}

FirebaseAdmin/FirebaseAdmin.Tests/Auth/Jwt/JwtTestUtils.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,80 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System;
1516
using System.Collections.Generic;
1617
using System.Collections.Immutable;
1718
using System.IO;
1819
using System.Security.Cryptography;
1920
using System.Security.Cryptography.X509Certificates;
2021
using System.Threading;
2122
using System.Threading.Tasks;
23+
using FirebaseAdmin.Auth.Tests;
24+
using FirebaseAdmin.Tests;
25+
using FirebaseAdmin.Util;
2226
using Google.Apis.Auth.OAuth2;
27+
using Google.Apis.Util;
28+
using Xunit;
2329

2430
namespace FirebaseAdmin.Auth.Jwt.Tests
2531
{
2632
public sealed class JwtTestUtils
2733
{
34+
internal const string ProjectId = "test-project";
35+
36+
internal static readonly IClock Clock = new MockClock();
37+
2838
internal static readonly IPublicKeySource DefaultKeySource = new FileSystemPublicKeySource(
2939
"./resources/public_cert.pem");
3040

3141
internal static readonly ISigner DefaultSigner = CreateTestSigner(
3242
"./resources/service_account.json");
3343

44+
public static AuthBuilder AuthBuilderForTokenVerification(string tenantId = null)
45+
{
46+
return new AuthBuilder
47+
{
48+
ProjectId = ProjectId,
49+
Clock = Clock,
50+
KeySource = DefaultKeySource,
51+
RetryOptions = RetryOptions.NoBackOff,
52+
TenantId = tenantId,
53+
};
54+
}
55+
56+
public static MockTokenBuilder IdTokenBuilder(string tenantId = null)
57+
{
58+
return new MockTokenBuilder
59+
{
60+
ProjectId = ProjectId,
61+
Clock = Clock,
62+
Signer = JwtTestUtils.DefaultSigner,
63+
IssuerPrefix = "https://securetoken.google.com",
64+
Uid = "testuser",
65+
TenantId = tenantId,
66+
};
67+
}
68+
69+
public static MockTokenBuilder SessionCookieBuilder(string tenantId = null)
70+
{
71+
return new MockTokenBuilder
72+
{
73+
ProjectId = ProjectId,
74+
Clock = Clock,
75+
Signer = JwtTestUtils.DefaultSigner,
76+
IssuerPrefix = "https://session.firebase.google.com",
77+
Uid = "testuser",
78+
TenantId = tenantId,
79+
};
80+
}
81+
82+
public static void AssertRevocationCheckRequest(string tenantId, Uri uri)
83+
{
84+
var tenantInfo = tenantId != null ? $"/tenants/{tenantId}" : string.Empty;
85+
var expectedPath = $"/v1/projects/{ProjectId}{tenantInfo}/accounts:lookup";
86+
Assert.Equal(expectedPath, uri.PathAndQuery);
87+
}
88+
3489
private static ISigner CreateTestSigner(string filePath)
3590
{
3691
var credential = GoogleCredential.FromFile(filePath);

0 commit comments

Comments
 (0)