I'm using Firebase authorization from a Flutter app which gets a JWT from the frontend like so:
accessToken = await user!.getIdToken(true);
Now I want my ASP.NET Core 5 backend app to verify the ID token.
I add the Firebase admin SDK to my .NET app like so:
In startup.cs
:
FirebaseApp.Create();
The Firebase docs show how to verify the ID token:
FirebaseToken decodedToken = await FirebaseAuth.DefaultInstance
.VerifyIdTokenAsync(idToken);
string uid = decodedToken.Uid;
But where does this code go? It's not something I want to manually do for every controller endpoint in my ASP.NET Core app. Can this be done as middleware to occur automatically on every API request?
I don't even think Firebase Admin is what I want. I just want to verify the JWT ID token which is created on the client using Firebase Auth for Flutter, in the most "Firebase way" possible. It is not for admins, it is for normal users. I thought Firebase Admin was that.
I essentially followed this SO answer for the non-Firebase stuff and this tutorial for the Firebase stuff, and it all works, I have access to the user, the access token, the claims, everything, through the extendedContext.userResolverService, and the id token is verified in all of my web controller endpoints that use [Authorize]
.
Since I have the ID token in all my controllers, I could manually call this from any controller too however it is not necessary.
FirebaseToken decodedToken = await FirebaseAuth.DefaultInstance
.VerifyIdTokenAsync(idToken);
string uid = decodedToken.Uid;
userResolverService.cs:
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
public class UserResolverService
{
public readonly IHttpContextAccessor _context;
public UserResolverService(IHttpContextAccessor context)
{
_context = context;
}
public string GetGivenName()
{
return _context.HttpContext.User.FindFirst(ClaimTypes.GivenName).Value;
}
public string GetSurname()
{
return _context.HttpContext.User.FindFirst(ClaimTypes.Surname).Value;
}
public string GetNameIdentifier()
{
return _context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
}
public string GetEmails()
{
return _context.HttpContext.User.FindFirst("emails").Value;
}
}
Extend the DataContext (via composition):
namespace Vepo.DataContext {
public class ExtendedVepoContext
{
public VepoContext _context;
public UserResolverService _userResolverService;
public ExtendedVepoContext(VepoContext context, UserResolverService userService)
{
_context = context;
_userResolverService = userService;
_context._currentUserExternalId = _userResolverService.GetNameIdentifier();
}
}
}
startup.cs:
public void ConfigureServices(IServiceCollection services)
{
....
services.AddHttpContextAccessor();
services.AddTransient<UserResolverService>();
services.AddTransient<ExtendedVepoContext>();
FirebaseApp.Create(new AppOptions()
{
Credential = GoogleCredential.FromFile("firebase_admin_sdk.json"),
});
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://securetoken.google.com/my-firebase-app-id";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://securetoken.google.com/my-firebase-app-id",
ValidateAudience = true,
ValidAudience = "my-firebase-app-id",
ValidateLifetime = true
};
});
also startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, VepoContext context, ISearchIndexService searchIndexService)
{ ....
app.UseAuthentication();
app.UseAuthorization();
Then add auth into the controller endpoint like so:
[HttpPost]
[Authorize]
public async Task<ActionResult<GroceryItemGroceryStore>> PostGroceryItemGroceryStore(GroceryItemGroceryStore groceryItemGroceryStore)
{...
You could take it a step further, and do things with the user on every save etc like add metadata:
the entity to save with metadata added:
public interface IDomainEntity<TId>
{
TId Id { get; set; }
DateTime SysStartTime { get; set; }
DateTime SysEndTime { get; set; }
string CreatedById { get; set; }
User CreatedBy { get; set; }
string UpdatedById { get; set; }
User UpdatedBy { get; set; }
}
my DataContext:
public class VepoContext : DbContext
{
public VepoContext(DbContextOptions<VepoContext> options)
: base(options)
{
}
public DbSet<User> User { get; set; }
public string _currentUserExternalId;
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
var user = await User.SingleAsync(x => x.Id == _currentUserExternalId);
AddCreatedByOrUpdatedBy(user);
return (await base.SaveChangesAsync(true, cancellationToken));
}
public override int SaveChanges()
{
var user = User.Single(x => x.Id == _currentUserExternalId);
AddCreatedByOrUpdatedBy(user);
return base.SaveChanges();
}
public void AddCreatedByOrUpdatedBy(User user)
{
foreach (var changedEntity in ChangeTracker.Entries())
{
if (changedEntity.Entity is IDomainEntity<int> entity)
{
switch (changedEntity.State)
{
case EntityState.Added:
entity.CreatedBy = user;
entity.UpdatedBy = user;
break;
case EntityState.Modified:
Entry(entity).Reference(x => x.CreatedBy).IsModified = false;
entity.UpdatedBy = user;
break;
}
}
}
}