3

I'm using firebase auth from a flutter app which gets a jwt from the front end like so:

accessToken = await user!.getIdToken(true);

Now I want my .net 5 AspNetCore back end 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 AspNetCore app. Can this be done as middleware to occur automatically on every api request?

EDIT: Eeek 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 firebasey way possible. It is not for admins, it is for normal users. I thought Firebase Admin was that. What direction should I take? I have found all sorts of articles online about different strategies so am confused which is the right one. Is this sufficient? https://dominique-k.medium.com/using-firebase-jwt-tokens-in-asp-net-core-net-5-834c43d4aa00 It doesn't seem very firebasey and seems quite over simplified? I think Dharmaraj's answer below is the most firebasey way, but I'm unsure where idToken gets a value from (how to get from the request headers? Otherwise it is just null).

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287

3 Answers3

6

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;
                    }
                }
            }
        }
BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287
  • Can anyone add a sample json file of what the config file looks like? – Nick Gallimore Jun 06 '23 at 19:27
  • 1
    @NickGallimore Do you mean "firebase_admin_sdk.json" in my example? Mine looks like [the json in this question](https://stackoverflow.com/q/40799258/3935156). Don't manually edit it. Pretty sure the file gets generated by [doing this](https://clemfournier.medium.com/how-to-get-my-firebase-service-account-key-file-f0ec97a21620). I just named my file differently. – BeniaminoBaggins Jun 07 '23 at 08:13
  • 1
    Yup, Google generates it for you thanks – Nick Gallimore Jun 08 '23 at 17:37
1

The "Admin" word in Firebase Admin SDK name implies that you use this SDK in trusted environments, such as you development machine, or a server that you control - such as is the case with your ASP.NET code.

The Admin SDK can verify tokens from any kind of user. In fact, it has no knowledge of types of users, it just verifies whether the token is valid.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thanks, it sounds suitable for my ASP.NET back end. I'm just not sure how to implement it so that it runs on every request. – BeniaminoBaggins Jun 24 '21 at 20:02
  • I'm not sure what you're having problems with, but I can tell you what Firebase itself does. The SDKs send the ID token in the `Authorization` heading of each request it makes to the backend services. The services get the token, and decode it in the same way the Admin SDK does, and then either pass the information on (like to security rules) or check if the user is authorized for the operation (such as for calls on behalf of the user to the Auth SDK backend APIs itself). – Frank van Puffelen Jun 24 '21 at 20:16
  • Yes thanks that sounds like what I want, but in the example code in my question which is also in the official docs, if anyone copy pasted that into their code, it will have a compile time error because the idToken variable is plucked out of thin air. – BeniaminoBaggins Jun 25 '21 at 00:37
  • Yeah, there might be a mistake in the variable name between those two snippets. If you think that is the problem, there should be a feedback button at the bottom of the page to report it. – Frank van Puffelen Jun 25 '21 at 01:24
-1

You add the functions which authenticate requests in a middleware. I'm am not sure about how that goes in .NET but I looked up for some middleware snippet and found this:

public class Startup
{
    public Startup()
    {
    } 

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
         app.Run(MyMiddleware);
    }

    private Task MyMiddleware(HttpContext context) 
    {
        // Add the code here and verify the IDToken
        //FirebaseToken decodedToken = await FirebaseAuth.DefaultInstance
    .VerifyIdTokenAsync(idToken); 
        //string uid = decodedToken.Uid;
        return context.Response.WriteAsync("Hello World! ");
    }
}

Now that middleware will check for the token for all the specified endpoints. You can easily read for the idToken (from headers or body) in there and return an error in case of any issue.

If the token is valid, you can add the user's UID and any other info that you require to the context object.

Dharmaraj
  • 47,845
  • 8
  • 52
  • 84