Skip to content

Commit 68c13fe

Browse files
authored
chore(auth): Added integration tests and snippets for session management (#178)
* chore(auth): Added integration tests and snippets for cookie management * Added extra cookie options * Cleaned up snippet code after testing
1 parent a59b4d7 commit 68c13fe

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,39 @@ public async Task SignInWithEmailLink()
487487
}
488488
}
489489

490+
[Fact]
491+
public async Task SessionCookie()
492+
{
493+
var customToken = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("testuser");
494+
var idToken = await SignInWithCustomTokenAsync(customToken);
495+
496+
var options = new SessionCookieOptions()
497+
{
498+
ExpiresIn = TimeSpan.FromHours(1),
499+
};
500+
var sessionCookie = await FirebaseAuth.DefaultInstance.CreateSessionCookieAsync(
501+
idToken, options);
502+
var decoded = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(sessionCookie);
503+
Assert.Equal("testuser", decoded.Uid);
504+
505+
await Task.Delay(1000);
506+
await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync("testuser");
507+
decoded = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(sessionCookie);
508+
Assert.Equal("testuser", decoded.Uid);
509+
510+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
511+
async () => await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(
512+
sessionCookie, true));
513+
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
514+
Assert.Equal(AuthErrorCode.RevokedSessionCookie, exception.AuthErrorCode);
515+
516+
idToken = await SignInWithCustomTokenAsync(customToken);
517+
sessionCookie = await FirebaseAuth.DefaultInstance.CreateSessionCookieAsync(
518+
idToken, options);
519+
decoded = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(sessionCookie, true);
520+
Assert.Equal("testuser", decoded.Uid);
521+
}
522+
490523
private static async Task<UserRecord> CreateUserForActionLinksAsync()
491524
{
492525
var randomUser = RandomUser.Create();

FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Google.Apis.Auth" Version="1.40.0" />
12+
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.0" />
13+
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
1214
</ItemGroup>
1315

1416
<ItemGroup>

FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
using System.Collections.Generic;
1717
using System.Threading.Tasks;
1818
using FirebaseAdmin.Auth;
19+
using Microsoft.AspNetCore.Http;
20+
using Microsoft.AspNetCore.Mvc;
1921

2022
namespace FirebaseAdmin.Snippets
2123
{
@@ -346,5 +348,176 @@ internal static async Task GenerateSignInWithEmailLink()
346348
// Place holder method to make the compiler happy. This is referenced by all email action
347349
// link snippets.
348350
private static void SendCustomEmail(string email, string displayName, string link) { }
351+
352+
public class LoginRequest
353+
{
354+
public string IdToken { get; set; }
355+
}
356+
357+
public class SessionCookieSnippets : ControllerBase
358+
{
359+
// [START session_login]
360+
// POST: /sessionLogin
361+
[HttpPost]
362+
public async Task<ActionResult> Login([FromBody] LoginRequest request)
363+
{
364+
// Set session expiration to 5 days.
365+
var options = new SessionCookieOptions()
366+
{
367+
ExpiresIn = TimeSpan.FromDays(5),
368+
};
369+
370+
try
371+
{
372+
// Create the session cookie. This will also verify the ID token in the process.
373+
// The session cookie will have the same claims as the ID token.
374+
var sessionCookie = await FirebaseAuth.DefaultInstance
375+
.CreateSessionCookieAsync(request.IdToken, options);
376+
377+
// Set cookie policy parameters as required.
378+
var cookieOptions = new CookieOptions()
379+
{
380+
Expires = DateTimeOffset.UtcNow.Add(options.ExpiresIn),
381+
HttpOnly = true,
382+
Secure = true,
383+
};
384+
this.Response.Cookies.Append("session", sessionCookie, cookieOptions);
385+
return this.Ok();
386+
}
387+
catch (FirebaseAuthException)
388+
{
389+
return this.Unauthorized("Failed to create a session cookie");
390+
}
391+
}
392+
393+
// [END session_login]
394+
395+
// [START session_verify]
396+
// POST: /profile
397+
[HttpPost]
398+
public async Task<ActionResult> Profile()
399+
{
400+
var sessionCookie = this.Request.Cookies["session"];
401+
if (string.IsNullOrEmpty(sessionCookie))
402+
{
403+
// Session cookie is not available. Force user to login.
404+
return this.Redirect("/login");
405+
}
406+
407+
try
408+
{
409+
// Verify the session cookie. In this case an additional check is added to detect
410+
// if the user's Firebase session was revoked, user deleted/disabled, etc.
411+
var checkRevoked = true;
412+
var decodedToken = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(
413+
sessionCookie, checkRevoked);
414+
return ViewContentForUser(decodedToken);
415+
}
416+
catch (FirebaseAuthException)
417+
{
418+
// Session cookie is invalid or revoked. Force user to login.
419+
return this.Redirect("/login");
420+
}
421+
}
422+
423+
// [END session_verify]
424+
425+
// [START session_clear]
426+
// POST: /sessionLogout
427+
[HttpPost]
428+
public ActionResult ClearSessionCookie()
429+
{
430+
this.Response.Cookies.Delete("session");
431+
return this.Redirect("/login");
432+
}
433+
434+
// [END session_clear]
435+
436+
// [START session_clear_and_revoke]
437+
// POST: /sessionLogout
438+
[HttpPost]
439+
public async Task<ActionResult> ClearSessionCookieAndRevoke()
440+
{
441+
var sessionCookie = this.Request.Cookies["session"];
442+
try
443+
{
444+
var decodedToken = await FirebaseAuth.DefaultInstance
445+
.VerifySessionCookieAsync(sessionCookie);
446+
await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(decodedToken.Uid);
447+
this.Response.Cookies.Delete("session");
448+
return this.Redirect("/login");
449+
}
450+
catch (FirebaseAuthException)
451+
{
452+
return this.Redirect("/login");
453+
}
454+
}
455+
456+
// [END session_clear_and_revoke]
457+
458+
internal async Task<ActionResult> CheckAuthTime(string idToken)
459+
{
460+
// [START check_auth_time]
461+
// To ensure that cookies are set only on recently signed in users, check auth_time in
462+
// ID token before creating a cookie.
463+
var decodedToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken);
464+
var authTime = new DateTime(1970, 1, 1).AddSeconds(
465+
(long)decodedToken.Claims["auth_time"]);
466+
467+
// Only process if the user signed in within the last 5 minutes.
468+
if (DateTime.UtcNow - authTime < TimeSpan.FromMinutes(5))
469+
{
470+
var options = new SessionCookieOptions()
471+
{
472+
ExpiresIn = TimeSpan.FromDays(5),
473+
};
474+
var sessionCookie = await FirebaseAuth.DefaultInstance.CreateSessionCookieAsync(
475+
idToken, options);
476+
// Set cookie policy parameters as required.
477+
this.Response.Cookies.Append("session", sessionCookie);
478+
return this.Ok();
479+
}
480+
481+
// User did not sign in recently. To guard against ID token theft, require
482+
// re-authentication.
483+
return this.Unauthorized("Recent sign in required");
484+
// [END check_auth_time]
485+
}
486+
487+
internal async Task<ActionResult> CheckPermissions(string sessionCookie)
488+
{
489+
// [START session_verify_with_permission_check]
490+
try
491+
{
492+
var checkRevoked = true;
493+
var decodedToken = await FirebaseAuth.DefaultInstance.VerifySessionCookieAsync(
494+
sessionCookie, checkRevoked);
495+
object isAdmin;
496+
if (decodedToken.Claims.TryGetValue("admin", out isAdmin) && (bool)isAdmin)
497+
{
498+
return ViewContentForAdmin(decodedToken);
499+
}
500+
501+
return this.Unauthorized("Insufficient permissions");
502+
}
503+
catch (FirebaseAuthException)
504+
{
505+
// Session cookie is invalid or revoked. Force user to login.
506+
return this.Redirect("/login");
507+
}
508+
509+
// [END session_verify_with_permission_check]
510+
}
511+
512+
private static ActionResult ViewContentForUser(FirebaseToken token)
513+
{
514+
return null;
515+
}
516+
517+
private static ActionResult ViewContentForAdmin(FirebaseToken token)
518+
{
519+
return null;
520+
}
521+
}
349522
}
350523
}

0 commit comments

Comments
 (0)