Firebase Multitenancy & .NET 7

Firebase Multitenancy & .NET 7

Introduction

Firebase is a leading platform for developing mobile and web applications, offering a variety of tools and services to make development straightforward and efficient. It’s filled with features that make creating, deploying, and managing applications a breeze, becoming a vital resource for developers who want reliable and efficient tools. One important feature of Firebase is multitenancy, allowing developers to build versatile applications that can serve different groups, each with their own set of data and functionality.

With multitenancy in Firebase, you can build scalable solutions that can serve varied users without sacrificing data integrity and user experience. It lets you create individual environments within your app to make sure each user group has a secure and personalized experience.

In this article, we will explore how to implement multitenancy in Firebase using .NET 7. We will look into the essential principles and advanced approaches for integrating multitenancy in the .NET environment effectively. We’ll use the Carcass framework, a creative, community-supported, open-source infrastructure for building modern and scalable .NET 7 applications. Whether you're an experienced developer or just starting, this article will provide the knowledge and insights you need to utilize Firebase multitenancy effectively in .NET 7 development.


Prerequisites

Before diving into Firebase multitenancy, make sure you have created a Firebase project with a configured Firebase Authentication.

  1. Sign in to Firebase
  2. Click Go to console.
  3. Click?+ Add project?and follow the prompts to create a project. You can name your project?anything you want.
  4. The project configuration page is open once the project is created.
  5. See the documentation to enable multitenancy for the Identity Platform.


Overview of Implementation

FirebaseAuthenticationHandler

The FirebaseAuthenticationHandler class is crucial for ensuring the security and integrity of the requests, it’s responsible for managing authentication processes.

public sealed class FirebaseAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private const string BearerPrefix = "Bearer ";

    private readonly FirebaseApp _firebaseApp;

    public FirebaseAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        FirebaseApp firebaseApp
    ) : base(options, logger, encoder, clock)
    {
        ArgumentVerifier.NotNull(firebaseApp, nameof(firebaseApp));

        _firebaseApp = firebaseApp;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Context.Request.Headers.ContainsKey("Authorization"))
            return AuthenticateResult.NoResult();

        string? authorizationHeaderValue = Context.Request.Headers["Authorization"];
        if (authorizationHeaderValue is null || !authorizationHeaderValue.StartsWith(BearerPrefix))
            return AuthenticateResult.Fail("Invalid scheme.");

        string idToken = authorizationHeaderValue[BearerPrefix.Length..];

        try
        {
            FirebaseToken firebaseToken = await FirebaseAuth.GetAuth(_firebaseApp).VerifyIdTokenAsync(idToken);
            ClaimsPrincipal claimsPrincipal = new(new List<ClaimsIdentity>
                {
                    new(firebaseToken.Claims
                            .Select(kvp => new Claim(
                                kvp.Key,
                                kvp.Value.ToString() ?? string.Empty)
                            ).ToList(),
                        nameof(ClaimsIdentity)
                    )
                }
            );

            return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, JwtBearerDefaults.AuthenticationScheme));
        }
        catch (Exception exception)
        {
            return AuthenticateResult.Fail(exception);
        }
    }
}        

FirebaseAuthenticationHandler authenticates requests by validating the Bearer token from the Authorization header, utilizing Firebase SDK to verify ID tokens. Here’s a simplified flow:

  • Extract the Authorization header.
  • Validate Bearer token.
  • If valid, create ClaimsPrincipal and return a successful AuthenticationTicket.
  • If invalid, return an authentication failure.

FirebaseUserAccessor

FirebaseUserAccessor plays a vital role in extracting user details from claims. It implements IFirebaseUserAccessor, IUserIdAccessor, and ITenantIdAccessor, making it versatile and central in accessing user and tenant details.

public sealed class FirebaseUserAccessor : IFirebaseUserAccessor, IUserIdAccessor, ITenantIdAccessor
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IJsonProvider _jsonProvider;

    public FirebaseUserAccessor(IHttpContextAccessor httpContextAccessor, IJsonProvider jsonProvider)
    {
        ArgumentVerifier.NotNull(httpContextAccessor, nameof(httpContextAccessor));
        ArgumentVerifier.NotNull(jsonProvider, nameof(jsonProvider));

        _httpContextAccessor = httpContextAccessor;
        _jsonProvider = jsonProvider;
    }

    public FirebaseUser? GetFirebaseUser()
    {
        ClaimsPrincipal? claimsPrincipal = _httpContextAccessor.HttpContext?.User;

        if (claimsPrincipal?.Identity is { IsAuthenticated: true })
        {
            List<Claim> claims = claimsPrincipal.Claims.ToList();

            string? id = claims.TryGetClaim("user_id");
            string? email = claims.TryGetClaim("email");
            bool emailVerified = bool.Parse(claims.TryGetClaim("email_verified") ?? "false");
            string? username = claims.TryGetClaim("username");

            JsonObject firebase = _jsonProvider.Deserialize<JsonObject>(claims.TryGetClaim("firebase")!);
            string? tenant = null;
            if (firebase.TryGetPropertyValue("tenant", out JsonNode? jsonNode))
                tenant = jsonNode!.GetValue<string>();

            return new FirebaseUser(id, email, emailVerified, username, tenant);
        }

        return null;
    }

    public string? TryGetUserId() => GetFirebaseUser()?.Id;
    public string? TryGetTenantId() => GetFirebaseUser()?.Tenant;
}        

FirebaseUserAccessor allows fetching the authenticated user's details, including Id, Email, Username, and Tenant, from the HttpContext. The GetFirebaseUser method retrieves the current authenticated user's information and constructs an FirebaseUser object. The TryGetUserId and TryGetTenantId methods are streamlined ways to access user and tenant IDs.

FirebaseUser

FirebaseUser is a model class representing a Firebase user. It holds various properties of a user, making it essential for managing user data effectively.

public sealed class FirebaseUser
{
    public FirebaseUser(
        string? id,
        string? email,
        bool emailVerified,
        string? username,
        string? tenant
    )
    {
        Id = id;
        Email = email;
        EmailVerified = emailVerified;
        Username = username;
        Tenant = tenant;
    }

    public string? Id { get; }
    public string? Email { get; }
    public bool EmailVerified { get; }
    public string? Username { get; }
    public string? Tenant { get; }
}        

ServiceCollectionExtensions

This class encompasses the registration of the Firebase App, FirebaseAuthenticationHandler, and FirebaseUserAccessor, orchestrating the integrity of the whole implementation.

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddFirebase(
        this IServiceCollection services,
        IConfiguration configuration
    )
    {
        ArgumentVerifier.NotNull(services, nameof(services));
        ArgumentVerifier.NotNull(configuration, nameof(configuration));

        services.Configure<FirebaseOptions>(configuration.GetSection("Firebase"));

        services.AddSingleton(sp =>
        {
            IOptions<FirebaseOptions> optionsAccessor = sp.GetRequiredService<IOptions<FirebaseOptions>>();

            return FirebaseApp.Create(new AppOptions
            {
                Credential = GoogleCredential.FromJson(optionsAccessor.Value.Json)
            });
        });

        return services;
    }

    public static IServiceCollection AddFirebaseAuthenticationHandler(this IServiceCollection services)
    {
        ArgumentVerifier.NotNull(services, nameof(services));

        services
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddScheme<AuthenticationSchemeOptions, FirebaseAuthenticationHandler>(JwtBearerDefaults.AuthenticationScheme, _ => { });

        return services;
    }

    public static IServiceCollection AddFirebaseUserAccessor(this IServiceCollection services)
    {
        ArgumentVerifier.NotNull(services, nameof(services));

        services
            .AddSingleton<IUserIdAccessor, FirebaseUserAccessor>()
            .AddSingleton<ITenantIdAccessor, FirebaseUserAccessor>()
            .AddSingleton<IFirebaseUserAccessor, FirebaseUserAccessor>();

        return services;
    }
}        

  • The AddFirebase method initializes and integrates the Firebase Application instance with the services collection, leveraging configurations to ensure the correct setup and availability of the Firebase app across the application.
  • The AddFirebaseAuthenticationHandler the method incorporates the custom FirebaseAuthenticationHandler into the application's authentication scheme, acting as a gatekeeper to validate incoming HTTP requests and ensuring interactions with secured API endpoints are authenticated and authorized.
  • The AddFirebaseUserAccessor method is pivotal for seamless access to user-related information by registering FirebaseUserAccessor as a singleton service, which grants versatile and consistent access to user and tenant IDs, ensuring user data availability throughout the application.


Summary

Firebase multitenancy, in conjunction with .NET 7, allows developers to architect scalable and robust applications that can efficiently serve multiple tenants within the same deployment, ensuring resource optimization and simplified management.

The code below illustrates an implementation where Firebase is crucial for controlling user identities and managing authentication, thereby ensuring that only verified users have access to the secured sections of the application. Multitenancy is seamlessly incorporated at multiple layers in this scenario, facilitating distinctive access and isolation driven by the tenant details connected to Firebase users.

For those interested in delving deeper into the implementation details or wishing to explore the intricacies of the code, the entire codebase featured in this article is available on GitHub.

And, to make things even easier, Carcass.Firebase is available as a NuGet package. You can add it to your project with this command:

dotnet add package Carcass.Firebase        

Feel free to explore, use, and contribute to the Carcass project!

要查看或添加评论,请登录

Serhii Kokhan的更多文章

  • AsyncLocal vs ThreadLocal

    AsyncLocal vs ThreadLocal

    ??Introduction Let’s be honest - working with async programming and multithreading in .NET can be a headache.

  • Enhancing .NET Logging with Serilog Custom Enrichers

    Enhancing .NET Logging with Serilog Custom Enrichers

    What Are Enrichers in Serilog? Enrichers in Serilog are components that automatically add extra context to log events…

    2 条评论
  • Data Synchronization in Chrome Extensions

    Data Synchronization in Chrome Extensions

    Introduction Data synchronization in Chrome extensions is a common challenge, especially for various tools ranging from…

  • Dalux Build API Changelog

    Dalux Build API Changelog

    Dalux unfortunately does not provide an official changelog for their API updates. To help developers stay informed, I…

    2 条评论
  • JSONB in PostgreSQL with EF Core - Part 2

    JSONB in PostgreSQL with EF Core - Part 2

    Introduction Welcome back to the second part of our series on using JSONB in PostgreSQL with EF Core. In our previous…

  • Proxy vs Reverse Proxy in the .NET 8 Universe

    Proxy vs Reverse Proxy in the .NET 8 Universe

    Today, we're diving into the world of proxies – but not just any proxies. We're talking about the classic proxy and its…

  • JSONB in PostgreSQL with EF Core

    JSONB in PostgreSQL with EF Core

    Introduction JSONB in PostgreSQL is a big step forward for database management. It mixes the best parts of NoSQL and…

    8 条评论
  • Mastering the use of System.Text.Json

    Mastering the use of System.Text.Json

    Introduction Handling JSON data is a daily task for many developers, given its widespread use in modern applications…

  • How to use Azure Maps in Blazor

    How to use Azure Maps in Blazor

    Introduction Blazor, a powerful and versatile framework for building web applications, allows developers to utilize C#…

  • Azure SQL Database Scaling

    Azure SQL Database Scaling

    Introduction In today's fast-paced digital world, enterprises must be agile and scalable to remain competitive. For…

社区洞察

其他会员也浏览了